
PCB视觉检测这几年在电子制造里基本成了标配但大多数公开案例都是Python一条龙训练脚本、推理服务、Web框架全堆在一起。真到了产线落地要跟已有的Java后端体系对接、要做数据落库、要搞实时告警Python方案往往就显得很“孤岛”。所以我这次直接选了Java YOLOv11 Spring Boot 3.4这套组合把检测链路完整落到一套可维护的服务架构里。这个项目的核心目标是给一条PCB生产线的AOI检测环节做一个替代人工复判的自动识别系统。产线相机拍下板卡图像后系统自动识别短路、断路、缺件、偏移、锡珠等缺陷类型把结果写进MySQL做追溯同时通过Redis支撑实时统计和阈值告警最终把异常工单推给产线看板。整套流程从模型训练到服务上线全部由我一个人从零搭建没有复用别人半成品。本文面向的是有一定Java基础、想了解深度学习模型如何用工程化方式嵌入业务系统的开发者以及正在评估PCB缺陷检测落地路径的制造行业从业者。我会把模型侧和服务侧的每一个关键决策、每一步实操都摊开讲包括那些文档里不会写的坑。1. 项目整体架构与选型思路1.1 缺陷检测的需求边界与技术路线PCB缺陷检测和通用目标检测最大的区别在于缺陷种类多、面积小、对比度低而且误报成本极高。少检一个短路可能会让整批板卡报废多报一个假缺陷又会拖垮产线效率。所以技术选型上首先要回答三个问题模型能不能识别小目标推理延迟能不能跟上产线节拍结果能不能被业务系统快速消费我的方案拆成四层图像采集层产线相机或本地图片上传、推理服务层YOLOv11模型加载与检测、业务处理层Spring Boot处理检测结果、落库、规则判断、告警展示层Redis做实时计数与状态缓存配合SSE推送到看板。这套分层的好处是每一层都能独立压测和替换比如前期没有真实相机时可以直接用图片上传接口模拟。1.2 为什么是YOLOv11而不是老牌YOLOv5或RCNN系说实话两年前我自己做第一版检测时用的是YOLOv5s精度在常见缺陷上不错但对密集小目标比如0.5mm线宽上的断路表现一般。YOLOv11在C3k2模块和C2PSA注意力机制的加持下小目标特征提取明显更强而且anchor-free设计省掉了大量锚框调参工作——这对工程落地非常重要因为产线上的缺陷尺寸分布非常不均匀锚框参数很难一次调好。更关键的是YOLOv11在推理速度上几乎没有牺牲精度的代价。用同一批PCB缺陷测试集对比v11s的mAP50比v5s高出3到4个点而单张640×640推理耗时在GPU上只多了不到2毫秒。在产线节拍是“每20秒出一张板”的场景下这个余量非常充足。1.3 Java技术栈各司其职的逻辑模型推理选型上有三条路Java直接调ONNX Runtime、调用独立的Python推理微服务、用DJL封装PyTorch模型。我最终选了ONNX Runtime Java API原因很务实少了Python进程间通信的延迟和运维负担一个JVM进程里既能跑推理又能跑业务逻辑部署就是打一个Jar包的事。Spring Boot 3.4在这里不只是Web框架它是整个检测结果的编排中心。MySQL负责缺陷记录、工单、设备的维度存储Redis负责两块脏活一是检测结果的实时计数比如当前班次某类缺陷出现频次二是告警去重和冷却窗口的控制。MySQL扛持久化Redis扛时效性各管一摊互不干扰。2. YOLOv11模型训练与工程化落地2.1 数据集准备PCB缺陷样本从哪里来PCB缺陷数据集最大的痛点就是“不够脏”。公开的PCB数据集大多是实验室环境下拍摄的光照均匀、背景干净但产线真实图像有反光、有油污、有传送带震动拖影。我当时的做法是把公开数据作为预训练基础再从产线收集了约5000张真实图像做补充两类缺陷样本做到每类不低于800张。标注环节我用的工具是labelImg格式导出为YOLO的txt格式每个缺陷类别对应一个id。这里有个重要经验对于重叠缺陷比如短路区域同时有锡珠标注时只标主要缺陷而不是画多个重叠框。否则训练出来的模型在预测阶段会输出大量低置信度的重复框NMS反而会把正确结果滤掉。缺陷按实际产线命名习惯分成六类short_circuit、open_circuit、missing_part、shift、solder_bridge、scratch编号0到5。2.2 训练参数设置与效果评估训练配置上我用了YOLOv11s作为主力模型输入尺寸640×640batch_size设为16epochs设200轮。优化器用默认的SGD配合warmup策略前3轮warmup让学习率从0缓慢爬升到0.01避免初始梯度震荡。数据增强打开了mosaic、flipud、hsv_h等但对flipud要谨慎PCB板卡有明确的丝印方向垂直翻转会让字符朝向失真模型可能学到错误特征。我实测下来flipud打开后误检率大约上升了1.2%果断关掉。最终模型在验证集上的指标mAP50为0.972mAP50-95为0.891单张推理平均耗时9.8msNVIDIA T4。这个精度对产线复判场景已经够用但注意mAP高并不代表产线没有问题——后面我会单独讲真实场景下阈值怎么调。2.3 模型导出为ONNX并与Java衔接训练完成后模型导出是关键一环。我直接用ultralytics包导出ONNX格式yolo export modelbest.pt formatonnx opset12 simplifyTrue导出后要检查ONNX的输入输出结构特别是输出维度。YOLOv11的ONNX输出通常是一个1×84×8400的Tensor其中84表示4个框坐标 80个类别得分8400是三个尺度特征图上的预测框总数。但因为我只训了6类所以输出是1×10×8400。这里有个绕不开的坑ONNX Runtime直接输出的是原始预测张量需要自己写后处理代码做解码、过滤和NMS。后处理的逻辑是从8400个候选框中筛掉置信度低于阈值的再做类别级别NMS最后把归一化坐标换算回原图尺寸。这段逻辑我会在Spring Boot服务里用Java重写一遍下一节具体说。3. Spring Boot 3.4 后端服务实现3.1 工程初始化与依赖引入项目用Maven管理Spring Boot版本3.4.xJava版本21。ONNX Runtime的Java依赖坐标直接用官方提供的artifact版本要和导出ONNX时的opset兼容建议选一个较新的稳定版本避免旧版不识别新算子。dependency groupIdcom.microsoft.onnxruntime/groupId artifactIdonnxruntime/artifactId version1.19.2/version /dependency另外还用了Spring Data JPA操作MySQL、Spring Data Redis操作Redis、Spring Web的SSE做实时推送。Spring Boot 3.4里SSE的接口写起来非常简洁用SseEmitter就可以实现服务端向浏览器/看板单向推送。这个组合没有引入额外重型中间件部署上就是一个Jar包加一个本地Redis产线环境友好很多。3.2 模型加载与推理核心代码模型加载做成单例BeanSpring容器启动时就加载ONNX文件避免每次请求都重新读文件。OrtSession不是线程安全的所以推理时要用线程池做并发控制我用Executors.newFixedThreadPool(4)配合一个阻塞队列处理请求。推理流程的代码骨架如下Service public class InferenceService { private final OrtSession session; private final OnnxTensor inputTensor; public DetectionResult detect(BufferedImage image) { // 1. 图像缩放至640x640并做letterbox填充 // 2. 转换为CHW float数组归一化到0~1 // 3. 创建OnnxTensorshape为 [1,3,640,640] // 4. session.run() 得到float[][][]原始输出 // 5. 后处理置信度过滤 NMS } }这里要重点提醒letterbox的实现。很多人在Java里直接image.getScaledInstance(640, 640)缩放这会改变图像宽高比导致检测框坐标映射错位。正确做法是等比缩放后在短边用灰色RGB 114填充推理完成后做坐标逆映射时去掉填充区域。这段代码网上很多版本写得不对我排查过一个误检问题最后定位就是填充值用成了黑色导致图像边缘的暗色缺陷全被误检。3.3 坐标映射与业务实体落库检测结果映射回原图坐标是另一个雷区。ONNX输出的坐标是letterbox填充后的归一化坐标必须做两步逆变换先乘640得到填充图上的像素坐标再减去填充偏移除以缩放比例。我封装了一个DetectionBox类里面直接存储原图坐标、置信度、类别方便前端画框和数据库查询。数据库建模方面我设计了四张核心表检测记录表defect_record、图片表defect_image、设备表device_info、告警工单表alert_order。检测记录表是核心字段包括图片ID、设备ID、缺陷类别、置信度、x坐标、y坐标、宽度、高度、检测耗时、创建时间。索引上对create_time和defect_type建了联合索引因为最常见的查询是“某个时间段内某类缺陷出现了多少次”没有这个索引数据量到百万级后会明显变慢。3.4 检测接口的完整调用链路对外暴露的检测接口是一个POST接口接收MultipartFile图片内部流程如下接收图片 - 保存原图到本地磁盘 - 缩略图生成 - 推理检测 - 结果落库 - Redis计数更新 - 规则引擎判断是否告警 - SSE推送结果这个流程中保存原图和缩略图是必须的因为产线需要事后追溯“当时到底板卡长什么样”光靠数据库里的坐标框没法复现现场。缩略图我控制在200KB以内作为列表页的快速预览原图按日期分目录存储。图片存储路径与数据库记录通过一个唯一UUID关联避免文件名冲突。4. Redis实时告警链路与性能优化4.1 Redis在链路中承担的两类职责第一类职责是实时计数。产线看板需要展示“本班次各缺陷类型的出现次数”如果每次都去MySQL里COUNT(*)在每分钟几十条检测记录的频率下虽然也能扛住但看板是每秒刷新的长期跑下来会对数据库造成不必要的压力。所以我用Redis的INCR命令维护每个缺陷类型的计数Key例如defect:count:shift_20250101:open_circuit每小时或每班次结束再异步把计数批量写回MySQL持久化。第二类职责是告警去重和冷却。同一块板卡如果在短时间内连续检出同类缺陷系统不应该反复发告警刷屏。做法是每次命中告警规则时先尝试写入一个带过期时间的Redis Key比如alert:cooldown:device_03:short_circuit过期时间设为120秒。只有写入成功即Key之前不存在才真正触发告警推送否则直接忽略。这个机制用的是SETNX的原子性天然天然免疫并发问题。4.2 告警规则的动态配置与实时推送告警规则我没有写死在代码里而是设计成数据库表缓存的方式。规则的字段包括设备ID、缺陷类型、阈值数量、统计窗口比如10分钟、冷却时间、是否启用。当检测记录落库后规则引擎会做两件事一是统计当前窗口内的缺陷次数二是对比阈值判断是否触发。规则判断的统计次数也放在Redis里用ZSet按时间戳存储每次检测事件// 记录一次缺陷事件 stringRedisTemplate.opsForZSet().add( defect:event: deviceId : defectType, String.valueOf(System.currentTimeMillis()), System.currentTimeMillis() ); // 查询窗口内次数 Long count stringRedisTemplate.opsForZSet().count( defect:event: deviceId : defectType, startTime, endTime );为什么用ZSet而不是简单计数因为滑动窗口要支持“删除窗口外的旧事件”ZSet按时间戳排序后可以用removeRangeByScore把过期的成员清理掉统计永远只针对有效窗口。这个设计在产线“某台设备连续3块板卡都出现同一缺陷”的场景下特别好用能精准抓到设备状态异常而不是单个板卡的偶然问题。SSE推送部分我用SseEmitter维护了一个连接池检测结果一旦落库就向该设备关联的所有前端会话推送一条JSON消息格式如下{ type: DEFECT_ALERT, deviceId: AOI-03, defectType: short_circuit, confidence: 0.95, imageUrl: /images/20250101/a1b2c3d4.jpg, timestamp: 2025-01-01 10:23:45 }前端看板收到消息后立即弹窗并播放声光提示整个链路从图片上传到看板弹出实测在1秒内完成其中模型推理占9.8ms通信开销几乎可以忽略。4.3 性能瓶颈排查与参数调优整套服务上线初期遇到过两个性能瓶颈。第一个瓶颈是ONNX Runtime的线程配置。默认情况下OrtSession会占用CPU的所有物理核在产线服务器上跟其他业务进程争抢资源。解决方式是创建Session时指定OrtSession.SessionOptions的setIntraOpNumThreads(2)把推理线程数限制住实测在双路服务器上整体吞吐反而更稳定。第二个瓶颈是Redis连接池。Spring Boot默认的Lettuce连接池在短连接高并发下会出现连接等待我把连接池最大连接数调到了50并且把检测记录的Redis写入改为批量Pipeline模式——每10条记录或每500毫秒批量刷一次。这里有个取舍批量写入会增加一点内存开销但对Redis的压力大幅下降产线这种写入频率下非常划算。5. 实际落地中的问题排查与经验沉淀5.1 高频问题速查表现象根因解决方案检测框整体偏左上/右下letterbox填充偏移计算错误统一用填充后坐标减去填充偏移再除以缩放比同一缺陷输出多个重叠框NMS的IoU阈值设置过松把NMS的IoU阈值从0.5降到0.45反光板面误检率飙升训练数据缺少反光样本采集时增加不同光照角度并在训练集做亮度增强Redis告警频繁误触发冷却Key过期时间设置过短按产线节奏把冷却时间调至120秒以上SSE连接频繁断开代理层空闲超时前端加心跳重连服务端设置更长的超时时间连续推理后内存缓慢上涨OrtSession输入Tensor未关闭每次推理后调用OnnxTensor.close()释放native内存内存泄漏那个问题最隐蔽。ONNX Runtime的Java绑定时OnnxTensor持有的是native内存GC管不到如果不显式close时间长了JVM的堆内存没涨多少但容器内存持续攀升最后触发OOM。排查这类问题要用jstat配合top看进程RES内存不能只看堆内存。5.2 真实产线环境比实验室残酷得多模型精度在实验室测得很理想一上产线就露馅。最大问题是光线PCB板面的反光会让局部区域过曝缺陷特征被高光吃掉。我当时的对策是给产线相机加了偏振滤光片同时把模型推理前的预处理改成可配置参数在检测接口里支持传入曝光增益参数不同产线可以单独调。这个改动很小但对误检率的改善非常明显。另外一个容易被忽略的问题是图像的色彩空间。相机输出的是RGB但有些工业相机直接输出Bayer RAW格式如果服务端不做正确的颜色转换模型看到的图像和训练时的分布完全不同。我在接口层加了一个图像格式校验非RGB格式一律先转换再送推理避免脏数据进入模型。5.3 个人实操中的几个关键心得第一不要把YOLO当黑盒。模型输出的原始张量、NMS逻辑、坐标解码这些后处理代码我强烈建议自己写一遍。原因有两个一是第三方库的后处理实现差异会导致结果不一致二是出现问题排查时不懂解码逻辑的人会完全无从下手。我在这套系统里把后处理全部用Java重写了虽然只写了不到200行但对整个系统的掌控感完全不同。第二监控先行。我上线第一周就加了三个监控推理耗时P95、检测结果积压队列长度、Redis连接池使用率。没有这些指标产线一有波动你根本不知道是模型变慢、队列积压还是Redis扛不住。我用简单的定时任务把指标写入日志再用PrometheusGrafana做了可视化看板虽然前期多花了一天时间但后面救了我无数次。第三数据库和缓存的一致性要想清楚。这个系统的数据流是“检测结果先写Redis做实时展示再由定时任务批量写MySQL做持久化”。如果服务中途宕机Redis里可能会有少量未落库的记录。我的处理是加了一个本地文件补偿日志定时任务启动时优先补录上次未完成的批次。产线场景可以接受最多几秒的数据丢失但绝对不能接受重复计数。这套系统从零开始到产线试运行前后花了大概三周其中模型训练和调参占了大头服务端开发和对接其实很快。现在每天稳定处理上千张板卡图像告警准确率比人工复判提高了不少最直接的收益是复判人员的劳动强度降下来了缺陷批次拦截做到了秒级响应。我个人最大的体会是深度模型落地到业务系统难点从来不在训练出一个高精度模型而是如何用工程手段把模型“嵌”进一个稳定、可维护、可观测的生产链路里。如果你也在做类似的项目建议先别急着堆功能把模型后处理、坐标映射、告警去重这三块打扎实整个系统的地基就稳了大半。