ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

用PyTorch实现桩基图像多任务识别:桩型、纹理与八道筋

用PyTorch实现桩基图像多任务识别:桩型、纹理与八道筋 在实际桩基工程项目的现场记录里“桩型有派纹理带帅青龙八道筋”这类说法经常出现在质检人员的口头描述中。它其实概括了三类关键信息桩型类别、桩身表面纹理以及钢筋笼上规律布置的八道纵向主筋。把这三类信息从现场照片中快速识别出来几乎是所有桩基信息化系统的基础能力。本文围绕一个名为“青龙八道筋留图识别”的最小图像识别项目讲解如何用 PyTorch 实现多任务模型把现场拍摄的桩体照片自动转换成结构化结果并提供一套可复用的训练、验证和部署方法。这篇文章不是只讲理论而是直接面向一个可运行的小系统。你会先理解三个识别目标分别是什么再看到数据标注格式、模型结构、训练代码、推理接口和常见报错排查方式。无论你负责桩基质检系统还是需要做工业图像分类项目都可以把这套思路移植到自己的场景里。下面所有代码都用于说明实现路径实际项目落地时需要根据你的数据集、目录结构和部署环境做调整。1. 先搞清楚项目里的三个识别目标1.1 桩型识别从规格上先分类“桩型有派”中的“派”在工程系统里可以理解为桩的规格类型。常见的预制桩有 PHC、PC、PTC 等类型它们的混凝土强度等级、配筋方式和适用场景各不相同。现场人员拍照记录时桩身外观、端板形式和标识文字都可能作为判断依据。从图像识别角度桩型是一个典型的多分类任务。模型输入一张桩体照片输出“PHC”“PC”“PTC”中的一个标签。相比目标检测它不需要框出桩体位置只要求图片主体是桩身或端板区域因此可以使用分类网络快速落地。这里需要注意桩型分类不是多标签问题。一根桩的桩型应当是确定的模型输出的概率向量中只会取概率最高的那一类。后续代码里会使用CrossEntropyLoss处理这个分支。1.2 表面纹理判断桩身工艺特征“纹理带帅”中的“纹理”指的是桩身表面的工艺特征。常见的纹理形态有光面、刻痕、抗渗纹等。纹理是否规则、清晰能反映混凝土成型质量、脱模效果和模具状态。比如带有均匀防滑块或刻痕的管桩往往是为了提高与承台或土体的摩擦力。纹理识别的难点在于类间差异小、光照变化大。现场照片会在不同角度、不同光线条件下拍摄同一根桩的纹理可能看起来差异很大。因此数据增强在纹理分支上非常重要代码里会加入随机旋转、翻转、颜色抖动和轻微模糊帮助模型抓住纹理本身的规律而不是记住照片的拍摄环境。纹理分支仍然采用多分类。如果你需要把“有无纹理”作为二分类问题也可以把标签简化成texture: smooth和texture: ribbed两组代码结构不变。1.3 青龙八道筋钢筋配置的关键检测项“青龙八道筋”是本文项目命名的核心来源。在不少桩基设计图中钢筋笼的主筋会按 6、8、10 根等数量均匀布置8 根是一种常见配置。现场施工人员为了快速描述这种配置常把它叫成“八道筋”。“青龙”在这里更多是形容沿桩身规律布置的视觉效果并不是某种神秘符号。从检测任务看八道筋的识别目标有两个方向。一种是判断钢筋笼是否采用了八道纵向主筋输出一个二分类标签另一种是识别主筋数量输出 0 到 10 的数量值。为了降低标注和训练难度本文先采用二分类方案标签eight_bar取 1 表示当前桩身钢筋笼符合八道筋配置取 0 表示不是。如果你希望模型直接输出主筋数量可以把最后一个分类头改成回归头输出一个浮点数再用MSELoss训练。实际项目中返回数量比返回“是不是”更有用但对数据集质量要求也更高。1.4 为什么用“留图”方式落地标题里的“结缘留图”按工程系统的理解就是用户把现场照片留下来并上传到系统由后台模型返回识别结果。这种交互方式很适合移动端和现场质检流程检测人员拍摄照片系统自动识别桩型、纹理和钢筋配置把结果写入台账。相比手动填写记录“留图”有三个明显好处照片本身就是原始凭证后续出现争议时可以回溯。识别结果可以自动落到 Excel 或数据库中减少二次录入。积累大量图片后模型可以不断迭代识别准确率会随数据量提升。所以“留图”不只是上传功能更是数据闭环的一部分。下一节会围绕这个场景设计技术方案。2. 技术选型多任务分类比三个独立模型更合适2.1 任务是单标签还是多标签一个图片需要同时输出三个结果桩型、纹理、八道筋。如果做成三个独立模型每个模型只负责一个属性开发和维护成本都会增加。但观察任务结构可以发现三个属性都来自同一张桩体图片共享底层视觉特征比如轮廓、色块、纹理方向。因此更适合使用多任务模型即一个骨干网络提取特征再接多个分类头分别输出不同预测结果。这样做的好处是训练和推理都只需要加载一个模型。共享特征提取层可以减少参数量。三个任务之间的底层特征可以互相促进尤其是样本量不足时。需要注意多任务不是万能的。如果三个任务的数据差异很大或者某个任务会干扰另一个任务就需要给不同分支设置损失权重甚至在反向传播时冻结部分层。2.2 骨干网络对比骨干网络负责从图像中提取通用特征。针对桩基图像识别网络不需要特别大因为类别数量少图像内容相对固定。下面表格列出三个可选方案。模型参数量推理速度精度特点适用场景ResNet18约 11.2M快结构简单、稳定CPU 可运行学习环境、初步原型MobileNetV3-Small约 2.5M很快轻量适合边缘设备手机端、嵌入式设备EfficientNetV2-S约 21M中等精读高数据充足时效果好服务器端、数据集较大实际项目不要盲目追求大模型。如果训练数据只有几百张ResNet18 或者 MobileNetV3 足够。等到数据量到几千张、类别分布稳定之后再切换更大模型才有收益。2.3 项目环境依赖本文代码基于 PyTorch。建议使用 Python 3.9 或更高版本主要依赖如下pip install torch torchvision opencv-python pillow pandas flask onnxruntime环境说明torch和torchvision用于模型定义和训练。opencv-python用于图像读取和简单预处理。pandas用于读取 CSV 标注文件。pillow负责图片对象处理。flask用于搭建上传识别接口。onnxruntime用于部署阶段的模型推理。如果你的机器没有 GPU仍然可以跑通本文代码只是训练速度会慢一些。建议把图片尺寸控制在 224x224CPU 训练也能接受。2.4 项目目录结构下面是一个推荐的目录结构避免后续代码路径混乱pile_recognition/ ├── data/ │ ├── images/ │ │ ├── 001.jpg │ │ ├── 002.jpg │ │ └── ... │ └── annotations.csv ├── train.py ├── predict.py ├── app.py ├── models/ └── weights/data/images存放现场图片。data/annotations.csv存放标注结果。train.py负责训练。predict.py负责单张图片推理。app.py负责上传接口。models存放模型定义文件或导入代码。weights存放训练好的权重文件。实际项目建议把这个目录纳入 Git 管理但权重文件和图片数据不要直接提交到仓库避免仓库体积过大。3. 数据标注与预处理3.1 标注 CSV 设计桩基图像识别项目里标注是最消耗时间、最容易出错的部分。每张图片需要标注三个字段桩型、纹理、八道筋。推荐用 CSV 保存方便人工检查和后续追加数据。image_id,image_path,pile_type,texture,eight_bar 001,data/images/001.jpg,PHC,ribbed,1 002,data/images/002.jpg,PC,smooth,0 003,data/images/003.jpg,PTC,wire,1字段说明字段含义示例值image_id图片唯一标识001image_path图片相对路径data/images/001.jpgpile_type桩型类别PHC、PC、PTCtexture纹理类别smooth、ribbed、wireeight_bar是否为八道筋配置0 或 1标注时要注意eight_bar是数值 0/1不是字符串。pile_type和texture使用统一的英文字符串避免中文标签在跨平台时出现编码问题。3.2 数据加载器实现数据加载器的核心作用是把图片路径和标签转换成模型需要的张量。代码如下import os import torch from torch.utils.data import Dataset from PIL import Image class PileDataset(Dataset): def __init__(self, df, img_dir, transformNone): self.df df self.img_dir img_dir self.transform transform self.type_map {PHC: 0, PC: 1, PTC: 2} self.texture_map {smooth: 0, ribbed: 1, wire: 2} def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img_path os.path.join(self.img_dir, row[image_path]) image Image.open(img_path).convert(RGB) if self.transform: image self.transform(image) type_label torch.tensor(self.type_map[row[pile_type]], dtypetorch.long) texture_label torch.tensor(self.texture_map[row[texture]], dtypetorch.long) eight_label torch.tensor(float(row[eight_bar]), dtypetorch.float32) return image, type_label, texture_label, eight_label注意几个关键点图片统一转换成 RGB避免灰度图导致输入通道不一致。eight_label使用float32配合后面的BCEWithLogitsLoss。pile_type和texture如果 CSV 中出现未知值会在self.type_map[row[pile_type]]处抛KeyError这正是我们想要的效果能尽早发现标注错误。3.3 数据增强策略数据增强能显著提高模型泛化能力尤其是在现场图片数量有限时。训练集和验证集要使用不同的 transform。训练集可以适度增强验证集只做缩放和归一化。from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop((224, 224), scale(0.8, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) valid_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])这里RandomResizedCrop会随机裁剪图片模拟不同拍摄距离下的局部特征。ColorJitter模拟现场不同光照条件。对于纹理识别颜色抖动尤其重要因为阴影和曝光会明显改变纹理观感。不要对验证集加入随机增强否则验证指标会受随机因素干扰无法真实反映模型效果。4. 多任务模型与训练4.1 模型结构模型采用“共享骨干 三个分类头”的结构。骨干网络使用 ResNet18去掉最后的全连接层后把特征分别送入三个输出头。import torch.nn as nn import torchvision.models as models class PileMultiTask(nn.Module): def __init__(self, num_types3, num_textures3): super().__init__() self.backbone models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) in_features self.backbone.fc.in_features self.backbone.fc nn.Identity() self.type_head nn.Linear(in_features, num_types) self.texture_head nn.Linear(in_features, num_textures) self.eight_head nn.Linear(in_features, 1) def forward(self, x): feat self.backbone(x) type_logits self.type_head(feat) texture_logits self.texture_head(feat) eight_logits self.eight_head(feat) return type_logits, texture_logits, eight_logits这段代码的核心是self.backbone.fc nn.Identity()。如果不做这一步ResNet18 会直接输出 1000 类 ImageNet 预测结果后面的分类头就接收不到正确维度的特征。使用Identity后特征向量维度是 512。eight_head输出维度是 1没有经过 Sigmoid因为训练时使用BCEWithLogitsLoss会更稳定。4.2 损失函数为什么这样组合三个任务使用不同损失函数桩型分支使用CrossEntropyLoss因为它是多分类。纹理分支同样使用CrossEntropyLoss。八道筋分支使用BCEWithLogitsLoss因为它是二分类且模型输出的是 logit。总损失是三者之和loss loss_type loss_texture loss_eight如果发现某个任务精度明显偏低可以给对应损失乘上权重。例如loss 1.0 * loss_type 1.2 * loss_texture 0.8 * loss_eight权重越大模型越优先优化该任务。实际使用时建议先让三个任务权重都为 1.0 跑一轮观察哪个分支收敛慢再调整。4.3 训练代码训练脚本按最简方式实现包含一个 epoch 的训练和验证逻辑。import torch import torch.nn as nn from torch.utils.data import DataLoader def train_one_epoch(model, loader, optimizer, device): model.train() total_loss 0.0 for images, type_labels, texture_labels, eight_labels in loader: images images.to(device) type_labels type_labels.to(device) texture_labels texture_labels.to(device) eight_labels eight_labels.to(device).unsqueeze(1) optimizer.zero_grad() type_logits, texture_logits, eight_logits model(images) loss_type nn.CrossEntropyLoss()(type_logits, type_labels) loss_texture nn.CrossEntropyLoss()(texture_logits, texture_labels) loss_eight nn.BCEWithLogitsLoss()(eight_logits, eight_labels) loss loss_type loss_texture loss_eight loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(loader)训练主循环里需要保存验证集上表现最好的权重而不是最后几轮的权重best_f1 0.0 for epoch in range(epochs): train_loss train_one_epoch(...) val_metrics validate(model, val_loader, device) if val_metrics[f1] best_f1: best_f1 val_metrics[f1] torch.save(model.state_dict(), weights/best_model.pt)保存最佳权重的逻辑对工业项目尤其重要。训练后期模型可能过拟合最后几个 epoch 的权重并不一定是最优的。4.4 关键训练参数表训练参数的设置会直接影响收敛效果。下面是常见配置和解释。参数推荐值说明图片尺寸224x224兼顾精度和训练速度ResNet 标准输入batch_size16 或 32显存小用 16CPU 训练建议 8epoch30 到 50数据少时不要过大关注验证曲线学习率1e-4 到 3e-4使用 ImageNet 预训练权重时不宜过大优化器Adam简单稳定适合多任务损失学习率策略CosineAnnealingLR后期下降平缓精度更稳定如果训练曲线震荡严重可以先降低学习率。如果验证集误差不下降可以考虑增加数据量或增强强度而不是继续堆 epoch。5. 推理验证与模型导出5.1 单张图片推理训练完成后需要写一个推理脚本输入图片路径输出结构化结果。核心代码import torch from PIL import Image from torchvision import transforms id_to_type {0: PHC, 1: PC, 2: PTC} id_to_texture {0: smooth, 1: ribbed, 2: wire} def predict_single(model, image_path, device): model.eval() image Image.open(image_path).convert(RGB) tensor valid_transform(image).unsqueeze(0).to(device) with torch.no_grad(): type_logits, texture_logits, eight_logits model(tensor) type_id torch.argmax(type_logits, dim1).item() texture_id torch.argmax(texture_logits, dim1).item() eight_prob torch.sigmoid(eight_logits).item() return { pile_type: id_to_type[type_id], texture: id_to_texture[texture_id], eight_bar_prob: round(eight_prob, 4), eight_bar: 1 if eight_prob 0.5 else 0 }这里必须调用model.eval()把 dropout 和 batch normalization 切换到评估模式否则同一张图片多次推理结果可能不同。5.2 输出结果解读对于一张测试图片输出可能如下{ pile_type: PHC, texture: ribbed, eight_bar_prob: 0.9732, eight_bar: 1 }含义是模型认为这张桩体照片的桩型为 PHC纹理为刻痕八道筋置信度为 0.9732因此判定为八道筋配置。eight_bar_prob越接近 0 或 1说明模型越有把握。如果结果接近 0.5建议在系统里标记为“低置信度需要人工复核”。5.3 评估指标只打印准确率还不够。工业场景中更应该关注每个类别的精确率、召回率和 F1 值。可以单独写一个评估函数统计混淆矩阵。from sklearn.metrics import classification_report # 假设 all_type_preds 和 all_type_labels 分别存了批次预测结果 print(classification_report(all_type_labels, all_type_preds, target_names[PHC, PC, PTC]))对于八道筋任务因为正负样本可能不均衡建议同时观察recall和precision。如果只关心准确率模型很可能把所有图片都判成“不是八道筋”准确率也不低但没有实际价值。5.4 导出 ONNX训练好的 PyTorch 模型可以用 ONNX 导出方便在服务端或边缘设备上部署。dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, weights/pile_model.onnx, input_names[input], output_names[type_logits, texture_logits, eight_logits], dynamic_axes{ input: {0: batch}, type_logits: {0: batch}, texture_logits: {0: batch}, eight_logits: {0: batch} } )dynamic_axes允许推理时批量大小不固定。生产环境可以用onnxruntime加载这个模型避免每个服务都安装 PyTorch显著降低部署体积和内存占用。6. 实现“留图上传”识别接口6.1 Flask 接口为了让“结缘留图”真正可用需要提供一个 HTTP 上传接口。使用 Flask 实现很直接import os import time import tempfile from flask import Flask, request, jsonify app Flask(__name__) app.route(/predict, methods[POST]) def predict(): f request.files.get(image) if f is None or f.filename : return jsonify({code: 400, message: missing image}), 400 suffix os.path.splitext(f.filename)[1].lower() if suffix not in {.jpg, .jpeg, .png}: return jsonify({code: 400, message: unsupported image type}), 400 tmp_path os.path.join( tempfile.gettempdir(), fpile_{int(time.time() * 1000)}{suffix} ) f.save(tmp_path) try: result predict_single(model, tmp_path, device) return jsonify({code: 0, data: result}) except Exception as e: return jsonify({code: 500, message: str(e)}), 500 finally: os.remove(tmp_path) if __name__ __main__: model PileMultiTask() model.load_state_dict(torch.load(weights/best_model.pt, map_locationcpu)) model.eval() device torch.device(cpu) app.run(host0.0.0.0, port5000)这里需要注意临时文件的清理。无论推理成功还是失败finally都会删除临时图片避免服务器磁盘被占满。6.2 使用 curl 验证Flask 服务启动后可以在另一个终端发送请求curl -X POST http://127.0.0.1:5000/predict \ -F imagedata/images/001.jpg正常的响应如下{ code: 0, data: { pile_type: PHC, texture: ribbed, eight_bar_prob: 0.9732, eight_bar: 1 } }如果缺少文件字段会收到{ code: 400, message: missing image }先通过 curl 验证接口再去写前端页面能省去很多联调成本。6.3 接口返回设计接口返回建议统一包裹code字段而不是直接返回数据或抛异常。前端判断逻辑会简单很多code 0表示成功数据放在data中。code 400表示参数错误。code 500表示服务端异常。不要把模型预测的原始 logits 直接返回给前端前端只需要可读的标签和置信度。如果需要追溯可以在服务端记录一份完整日志但接口层保持精简。6.4 前端上传页面的关注点如果后续要写前端页面需要注意以下几点上传前在浏览器端校验图片格式和大小。使用FormData提交文件字段名要和后端request.files.get(image)保持一致。显示结果时把低置信度样本标黄提示人工复核。上传接口要设置超时时间防止大图导致服务阻塞。前端页面不是本文重点但接口设计必须为前端留出余地尤其是错误码和字段类型要保持稳定否则前端每兼容一次接口改动都会很痛苦。7. 常见问题排查7.1 图片上传报 404 或 400如果调用接口时出现 404先确认 Flask 服务是否启动、路由路径是否匹配。如果出现 400通常是缺少image字段或文件名为空。检查方式curl -X POST http://127.0.0.1:5000/predict -F imagedata/images/001.jpg -v加上-v可以看到 HTTP 请求和响应详情。只要不是参数问题基本都能定位到是路径不对还是文件字段不对。7.2 训练损失为 NaN训练时 loss 变成 NaN常见原因有学习率过大梯度爆炸。归一化参数错误导致输入张量包含异常值。标签越界例如pile_type取值不在 0 到 2 之间。使用了不稳定的损失组合。解决思路是先调小学习率比如从3e-4降到1e-5。再打印训练集的均值、方差确认归一化后的图像像素范围正常。最后检查标注 CSV 中是否有多余空格导致 label 映射失败。7.3 所有图片被预测成同一个类别最典型的原因是样本不均衡。比如 90% 的图片桩型标注为 PHC模型只要全部输出 PHC准确率也能到 90%但纹理和八道筋任务会失效。应对方法增加少数类样本或对少数类做更强的数据增强。在DataLoader中使用WeightedRandomSampler提高少数类采样概率。给少数类分支设置更高损失权重。不要一开始就换复杂模型先确认训练集类别分布是否合理。7.4 CPU 推理太慢如果模型在 CPU 上单张图片推理超过几百毫秒可以从以下几个方面优化图片输入尺寸从 224x224 降到 192x192 或 160x160。使用 MobileNetV3 替换 ResNet18。导出 ONNX用onnxruntime推理。如果 CPU 支持 AVX 指令安装带 AVX 优化的 PyTorch 或 ONNX Runtime 版本。对于现场管理系统单张图片推理速度在 1 秒内通常可以接受。如果并发量高还要考虑加 GPU 或限制队列长度。7.5 换数据集后维度不匹配把代码用到自己的数据上时最容易遇到“维度不匹配”错误。常见原因是类别数量对不上桩型从 3 类变成 4 类但PileMultiTask仍使用num_types3。纹理从 3 类变成 2 类但num_textures3。目标从二分类改成回归但eight_head输出维度仍是 1。每次改数据集都需要同步检查模型初始化参数和id_to_type、id_to_texture映射表。建议把类别数量和标签映射写成一个配置文件避免代码里硬编码。8. 生产环境落地建议8.1 学习环境与生产环境差异学习环境里训练脚本和推理脚本可以放在同一台机器上路径也可以写死。生产环境则需要补齐很多边界对比项学习环境生产环境配置文件硬编码在代码中环境变量或配置中心模型加载每次启动新建模型预加载并复用实体日志控制台打印结构化日志 监控告警异常处理直接打印堆栈统一错误码和链路追踪数据备份不强制标注数据和模型权重定期备份回滚不关注保留多个模型版本支持快速回退不要直接把训练脚本搬到生产环境。训练代码关注的是指标部署代码关注的是稳定性、并发和可观测性。8.2 模型版本管理与回滚模型文件应该像代码一样做版本管理。推荐命名格式pile_model_v1_20250101.pt pile_model_v2_20250601.pt每次发布模型时记录以下信息训练数据量和类别分布。验证集准确率、召回率、F1 值。模型结构和权重文件路径。发布时间和负责人。如果新模型出现明显误判旧模型权重还在可以快速切换回滚。8.3 数据标注质量检查数据标注决定了模型上限。建议在正式训练前检查标注一致性随机抽 50 张图让两个标注人员分别标注。比较两人标注结果计算一致率。一致率低于 90% 时先修订标注规范不要急着训练。对于桩基图片容易产生歧义的是纹理类别。建议在标注规范里附上每种纹理的示例图和文字描述减少主观判断差异。8.4 发布前检查清单在把系统发布到测试环境或生产环境前可以按下面的清单检查检查项检查内容权重文件是否存在是否对应最新模型类别映射id_to_type和id_to_texture是否与训练时一致图片预处理推理时是否使用了和训练相同的 transform接口字段返回字段名是否稳定是否有中英文混用异常路径缺少文件、图片损坏、超时是否有兜底返回日志记录是否记录了图片名称、推理结果、置信度和耗时权限控制上传接口是否需要登录鉴权是否限制文件类型和大小备份方案模型权重和 CSV 标注是否已备份每项都值得落到操作手册里而不是靠记忆。8.5 后续扩展方向这个项目把桩型、纹理、八道筋三个识别任务做成了多任务分类。后续可以扩展的方向包括从二分类八道筋改成主筋数量回归直接输出 6、8、10 等数值。把桩身区域检测和属性识别串联形成“检测 识别”两阶段流程。记录每张图片的拍摄时间和 GPS建立桩基质量档案。对低置信度结果自动进入人工复核队列持续补充难样本。实际工程中真正决定系统价值的并不是模型结构而是数据规范、部署稳定性和人工协作机制。先把最小可用系统跑通再逐步积累数据模型的精度和可信度才会稳步提升。
返回列表