1. 炉前烟尘识别到底在解决什么问题
第一次接触这个需求是在一个铜熔炼车间,炉长拉着我看炉口,说“你帮我看看现在这个烟算浓还是淡”。我当时愣了一下——这玩意儿肉眼判断全凭经验,老师傅说浓就浓,说淡就淡,换个人来看结论可能完全相反。更麻烦的是,环保数据要留痕,加料时机要卡点,全靠人盯炉口,一个班下来眼睛都花了,还容易漏看。
熔炼炉烟尘AI视觉识别要干的事情,说白了就是用摄像头替代人眼,把炉前烟尘状态实时分成浓烟、淡烟、无烟三类,输出结构化信号给中控系统。它解决的核心痛点有三个:第一,人工判断主观性强、标准不统一;第二,炉前环境高温高尘,人不能长时间靠近;第三,烟尘状态直接关联加料节奏、燃烧效率和排放合规,需要秒级响应。
这套方案适合谁参考?做工业视觉落地的算法工程师、冶金/铸造行业的自动化改造负责人、以及计算机视觉方向想找真实场景练手的学生。我后面会从方案选型、数据采集、模型训练、部署调优到现场踩坑,完整拆一遍,代码和参数都给到能直接抄的程度。
注意:工业现场和实验室跑demo完全是两码事。实验室里准确率99%的模型,到炉口可能连60%都保不住,原因后面会细说。
2. 整体方案设计与技术选型思路
2.1 为什么是“三分类”而不是“浓度回归”
很多人第一反应是做个回归模型,输出一个0到1的烟雾浓度值,看起来更精细。我一开始也这么想,但实际跑下来发现两个致命问题。
第一,标注一致性无法保证。让十个老师傅给同一段视频打浓度分,分数能差出0.3。你说0.6算浓还是淡?没有客观基准,回归目标的噪声太大,模型学出来的东西不可信。
第二,业务侧根本不需要连续值。中控系统要的是决策信号:浓烟了要不要加大引风?淡烟了要不要准备加料?无烟了是不是可以进入下一工序?三个离散状态足够驱动逻辑,反而更稳定、更好验证。
所以三分类是业务需求倒推出来的最优解,不是技术上的偷懒。分类边界清晰,标注可仲裁,模型输出可以直接映射到PLC动作。
2.2 视觉方案选型:可见光还是红外
炉前环境有两个极端:一是强光,炉火本身是巨大光源,普通相机容易过曝;二是烟尘遮挡,可见光在浓烟下穿透力有限。
我对比过三种方案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 工业可见光相机 | 成本低、色彩信息丰富 | 受炉火强光干扰大、浓烟下失效 | 无烟/淡烟区分 |
| 红外热成像 | 穿透烟尘能力强、不受可见光影响 | 成本高、分辨率低、纹理信息少 | 浓烟/无烟区分 |
| 可见光+红外融合 | 互补性强、鲁棒性最好 | 标定复杂、数据同步要求高 | 高要求场景 |
最终我选了工业可见光相机+窄带滤光片的方案。原因很实际:预算有限,而且三分类里“淡烟”这个中间态主要靠纹理和透明度判断,红外图像在这块信息不足。滤光片选的是850nm窄带,把炉火里最强的可见光波段压下去,同时保留烟尘的散射特征。
相机安装位置也有讲究。我试过正对炉口、侧45度、顶部俯视三个角度,最后定在侧上方30度、距离炉口约3.5米。正对容易被喷溅物打坏镜头,俯视看不到烟的扩散形态,侧上方既能拍到烟柱轮廓,又能避开大部分飞溅。
2.3 模型选型:轻量级CNN还是Transformer
2024年了,Transformer在视觉领域确实火,但工业部署要考虑推理延迟和硬件成本。炉前监测要求单帧推理小于50ms,才能做到真正的实时。
我最终选了MobileNetV3-Small做主干,输入分辨率224x224,参数量约2.5M,在Jetson Xavier NX上单帧推理约18ms。为什么不用ResNet50?因为现场部署的是边缘设备,功耗和散热都受限,大模型跑起来风扇狂转,车间温度又高,稳定性反而下降。
Transformer方案我也试过ViT-Tiny,准确率比MobileNetV3高约1.5个百分点,但推理时间翻倍到40ms左右,而且对训练数据量要求更高。在工业场景里,稳定性和延迟比那1.5个点重要得多。
实操心得:不要盲目追新架构。工业落地的第一原则是“够用就好”,模型小、推理快、容易维护,比刷榜重要。
3. 数据采集与标注的实操细节
3.1 现场采集:怎么拍到有用的数据
数据采集这一步,我踩的坑最多。第一次去现场,架好相机录了三天,回来一看,80%的帧都是无效的——要么炉门关着,要么画面里全是火花,要么曝光过度一片白。
后来总结出一套采集规范:
- 采集时段覆盖完整生产周期:加料、熔化、扒渣、出料,每个阶段都要有。浓烟主要出现在加料和扒渣阶段,无烟出现在出料后,淡烟是过渡态。
- 每类至少采集2000帧有效图像,且要覆盖不同班次、不同炉料、不同天气(车间光照受外界影响)。我最终攒了约12000帧原始数据,三分类大致均衡。
- 同步记录工艺参数:加料时间、引风频率、炉温。这些后面用来做数据校验,比如“引风开到80%还出现浓烟”就是异常样本,要单独分析。
- 固定相机参数:曝光、增益、白平衡全部手动锁定。自动模式在不同光照下会漂移,导致模型学到的是相机参数变化而不是烟尘变化。
采集脚本我用的是OpenCV,每500ms抓一帧,同时写时间戳:
import cv2 import time import os cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) # 手动锁定曝光,具体值现场调 cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) cap.set(cv2.CAP_PROP_EXPOSURE, -6) save_dir = "raw_data" os.makedirs(save_dir, exist_ok=True) count = 0 while True: ret, frame = cap.read() if not ret: break ts = time.strftime("%Y%m%d_%H%M%S") cv2.imwrite(f"{save_dir}/{ts}_{count:06d}.jpg", frame) count += 1 time.sleep(0.5)3.2 标注规范:让三个人标出一样的结果
标注是三分类项目里最容易被低估的环节。我一开始让两个实习生标,结果一致性只有70%左右。后来制定了明确的标注规则,一致性提到92%以上。
规则核心是看三个维度:烟柱的不透明度、扩散范围、边缘清晰度。
- 浓烟:烟柱完全不透明,看不到炉口背景,扩散范围超过炉口宽度2倍,边缘模糊。
- 淡烟:烟柱半透明,能隐约看到炉口轮廓,扩散范围在炉口宽度1到2倍之间,边缘有一定清晰度。
- 无烟:画面中无明显烟柱,或仅有极淡的热气流扰动,炉口背景清晰可见。
边界样本处理原则:拿不准的归到相邻更严重的类别。比如浓烟和淡烟之间拿不准,标浓烟。因为业务上漏报浓烟的代价比误报大得多。
标注工具我用的是LabelImg改的分类版本,每张图只打一个类别标签。12000张图,两个人标了约一周,交叉校验了两轮。
注意:标注时一定要把图像分成“训练集”和“验证集”两个文件夹分别标,避免同一段视频的相邻帧同时出现在训练和验证里,否则验证准确率会虚高。
4. 模型训练与调优的完整流程
4.1 数据增强:模拟现场的各种干扰
工业现场的图像变化比公开数据集复杂得多。我用了一套组合增强策略,在训练时动态应用:
import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform = A.Compose([ A.RandomResizedCrop(224, 224, scale=(0.7, 1.0)), A.HorizontalFlip(p=0.5), A.RandomBrightnessContrast(brightness_limit=0.3, contrast_limit=0.3, p=0.7), A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=20, val_shift_limit=20, p=0.5), A.GaussNoise(var_limit=(10.0, 50.0), p=0.3), A.MotionBlur(blur_limit=5, p=0.2), A.CoarseDropout(max_holes=8, max_height=16, max_width=16, p=0.3), A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ToTensorV2() ])这里有几个针对性的设计:
- RandomResizedCrop:模拟相机焦距变化和烟尘远近差异。
- RandomBrightnessContrast:模拟炉火强弱和曝光波动。
- MotionBlur:模拟烟尘快速流动时的运动模糊。
- CoarseDropout:模拟飞溅物短暂遮挡镜头。
验证集只用Resize和Normalize,不做增强,保证评估结果反映真实分布。
4.2 训练配置与类别权重
三分类虽然均衡,但实际生产中浓烟样本偏少(因为浓烟时段短),所以我在损失函数里加了类别权重。权重按类别频率的倒数计算:
import torch import torch.nn as nn from torchvision.models import mobilenet_v3_small # 假设训练集三类样本数为 [4000, 4500, 3500] class_counts = [4000, 4500, 3500] total = sum(class_counts) weights = [total / (3 * c) for c in class_counts] # 约 [1.0, 0.89, 1.14] class_weights = torch.tensor(weights, dtype=torch.float32).to(device) model = mobilenet_v3_small(pretrained=True) model.classifier[3] = nn.Linear(1024, 3) model = model.to(device) criterion = nn.CrossEntropyLoss(weight=class_weights, label_smoothing=0.1) optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30)训练超参:batch size 64,epoch 30,初始学习率1e-3,余弦退火。在RTX 3060上大约训练40分钟。
关键技巧:label_smoothing=0.1。工业数据标注难免有噪声,标签平滑能防止模型对边界样本过度自信,实测验证集准确率提升约2个百分点。
4.3 训练结果与混淆矩阵分析
最终模型在验证集上的表现:
| 类别 | 精确率 | 召回率 | F1 |
|---|---|---|---|
| 浓烟 | 0.96 | 0.94 | 0.95 |
| 淡烟 | 0.91 | 0.93 | 0.92 |
| 无烟 | 0.97 | 0.96 | 0.96 |
整体准确率约94%。混淆矩阵显示主要错误集中在淡烟和浓烟的边界,以及淡烟和无烟的边界。这符合预期,因为这两个边界本身就是连续过渡的。
我针对边界样本做了第二轮专项采集,在炉口加装了辅助光源,让淡烟的纹理更清晰,补充了约1500张边界样本重新训练,淡烟F1提升到0.94。
实操心得:不要只看整体准确率。工业场景里,漏报浓烟的代价最大,所以浓烟的召回率要单独盯。我最终把浓烟召回率调到0.97以上才敢上线。
5. 边缘部署与实时推理优化
5.1 模型导出与量化
训练完的PyTorch模型要部署到Jetson Xavier NX上,直接跑FP32推理延迟约35ms,不够理想。我做了两步优化:
第一步,导出ONNX:
dummy_input = torch.randn(1, 3, 224, 224).to(device) torch.onnx.export( model, dummy_input, "smoke_cls.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, opset_version=11 )第二步,用TensorRT做FP16量化:
trtexec --onnx=smoke_cls.onnx \ --saveEngine=smoke_cls_fp16.engine \ --fp16 \ --workspace=1024 \ --minShapes=input:1x3x224x224 \ --optShapes=input:1x3x224x224 \ --maxShapes=input:1x3x224x224量化后单帧推理降到12ms,加上预处理和后处理,端到端约20ms,满足50ms的实时要求。精度损失不到0.5个百分点,完全可接受。
5.2 推理服务与平滑策略
单帧分类结果会有抖动,比如浓烟和淡烟之间来回跳。直接输出给PLC会导致执行机构频繁动作。我加了一个滑动窗口投票:
from collections import deque import numpy as np class SmoothPredictor: def __init__(self, window_size=5, threshold=0.6): self.window = deque(maxlen=window_size) self.threshold = threshold def update(self, probs): self.window.append(probs) avg = np.mean(self.window, axis=0) pred = int(np.argmax(avg)) conf = float(avg[pred]) if conf < self.threshold: return -1 # 不确定,保持上一状态 return pred窗口大小5,对应约100ms的平滑。阈值0.6,低于这个值就输出“保持”,避免频繁切换。实测下来,状态切换频率从每秒3到4次降到每分钟2到3次,中控侧反馈很稳。
5.3 与PLC的通信对接
推理服务输出三分类结果后,通过Modbus TCP写给PLC。我用的是pymodbus:
from pymodbus.client import ModbusTcpClient client = ModbusTcpClient('192.168.1.10', port=502) client.connect() # 寄存器地址0x0001,0=无烟,1=淡烟,2=浓烟 def write_state(state): client.write_register(0x0001, state)PLC侧根据状态执行逻辑:浓烟触发引风加大,淡烟准备加料,无烟进入下一工序。整个链路从图像采集到PLC动作,端到端延迟约80ms。
注意:Modbus寄存器地址和数据类型一定要和PLC工程师对齐,我因为地址偏移搞错了一次,导致引风逻辑反了,炉子差点出问题。上线前务必做联调测试。
6. 现场常见问题与排查速查
6.1 模型在现场掉点怎么办
这是最典型的问题。实验室94%,现场可能只有80%出头。原因通常有三个:
第一,光照变化。车间白天和晚上光照差异大,虽然锁了相机参数,但外界光还是会漏进来。解决办法是在炉口加装遮光罩,同时训练时加入更强的亮度增强。
第二,镜头污染。炉前飞溅物和粉尘会逐渐糊住镜头,图像变模糊。我加了每4小时自动吹扫的气路,镜头前加保护玻璃,每周更换一次。同时训练时加入高斯模糊增强,让模型对轻度污染有鲁棒性。
第三,炉料变化。换了一种炉料后,烟的颜色和形态可能不同。解决办法是持续采集新数据,做增量训练。我一般每两周收集一批新样本,微调最后一层分类头。
6.2 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 准确率骤降 | 镜头污染 | 查看原始图像清晰度 | 吹扫/更换保护玻璃 |
| 浓烟漏报 | 曝光过度 | 检查图像是否过曝 | 调低曝光或加滤光片 |
| 状态频繁切换 | 平滑窗口太小 | 观察输出日志 | 增大窗口或提高阈值 |
| 推理延迟高 | 未量化 | 检查推理引擎 | 转TensorRT FP16 |
| 通信失败 | 地址错误 | 用Modbus调试工具 | 核对寄存器地址 |
| 夜间误报多 | 光照不足 | 对比白天夜间图像 | 补光或增强训练数据 |
6.3 独家避坑技巧
技巧一:保留“不确定”状态。三分类之外,我实际部署时加了一个隐式的第四态——置信度低于阈值时输出“保持”。这比强行分类要安全得多,避免模型在边界上乱跳。
技巧二:用工艺参数做交叉校验。如果引风频率已经开到90%还报浓烟,大概率是模型误判,可以触发人工复核。这个逻辑帮我过滤了约30%的误报。
技巧三:模型版本管理要严格。每次更新模型都要记录训练数据版本、超参、验证指标。我有一次直接覆盖了线上模型,结果新模型在某个班次表现很差,想回滚都找不到旧版本。后来用MLflow做版本管理,再没出过这个问题。
技巧四:现场调试带个便携屏。炉前环境嘈杂,抱着笔记本蹲在炉口调参很不现实。我后来带了一个带HDMI的便携屏和无线键鼠,在安全区域远程调试,效率高很多。
7. 后续可扩展的方向
这套三分类框架跑通后,其实可以往几个方向延伸。一是烟尘浓度分级细化,比如把浓烟再分成“浓”和“极浓”,对应不同的引风档位。二是多炉口联动,一个车间多个炉子,统一调度引风资源。三是结合炉温预测,烟尘状态和炉内反应阶段强相关,可以用来辅助判断熔化进度。
我目前在做的是把分类结果和能耗数据关联分析,看看能不能找到“烟尘状态-引风能耗-炉温”的最优控制曲线。初步数据看,淡烟阶段适当降低引风频率,能耗能降约8%,同时不影响排放指标。这个方向还在验证,有结果再单独写一篇。
代码和配置我都整理在了一个仓库里,核心就是MobileNetV3加TensorRT量化加滑动窗口平滑,没有花哨的东西,但现场跑了半年多,稳定性没问题。工业视觉落地,简单可靠比先进重要,这是我最大的体会。