第1642篇:制造业AI质检系统架构——Spring AI + 视觉模型的生产实践
第1642篇:制造业AI质检系统架构——Spring AI + 视觉模型的生产实践
去年接了个项目,甲方是做精密零部件的工厂,产线上的质检员每天要盯着显微镜看上千个零件,眼睛都快瞎了,还经常漏检。老板要做AI质检,预算给得挺足,但要求也高:漏检率不能超过0.1%,误检率不能超过5%。
我当时心里没底,这两个指标放在一起其实很矛盾。漏检率低了,误检率就容易高;误检率低了,漏检率又上去。最后怎么解决的,等我慢慢讲。
先搞清楚质检场景到底有多复杂
做之前我去工厂蹲了三天,跟质检工人一起上班,这三天收获巨大。
质检员判断一个零件是否合格,考虑的因素包括:
- 表面划痕(深度、长度、位置)
- 尺寸偏差(关键尺寸是否在公差范围内)
- 毛刺残留
- 氧化变色
- 形变(肉眼可见的弯曲或扭曲)
而且不同产品线、不同客户的标准还不一样。同一个划痕,A客户可以接受,B客户就要退货。
这给我的设计带来了第一个重要决策:质检模型不能是通用的,必须支持按产品线和客户标准做配置化。
整体架构是这样的:
注意中间那个分叉:先用轻量检测模型判断"有没有问题",再用视觉大模型分析"是什么问题、有多严重"。这是个很关键的设计,等会解释为什么。
图像采集与预处理
工厂环境里采集图像比想象的难多了。光线不稳定、零件反光、油污遮挡,这些都是噪音。
我们用工业相机(分辨率4096×3000)+ 环形光源,相机和光源控制器通过Modbus协议接入系统:
@Service
public class CameraService {
@Value("${camera.ip}")
private String cameraIp;
@Value("${camera.trigger-delay-ms}")
private int triggerDelayMs;
private GigECamera camera;
@PostConstruct
public void initCamera() {
CameraConfig config = CameraConfig.builder()
.ip(cameraIp)
.exposureTime(5000) // 微秒
.gain(1.0)
.triggerMode(TriggerMode.EXTERNAL) // 外部触发
.pixelFormat(PixelFormat.Mono8)
.build();
this.camera = new GigECamera(config);
this.camera.connect();
log.info("相机连接成功:{}", cameraIp);
}
/**
* 零件到位信号触发拍照
*/
@EventListener(PartArrivedEvent.class)
public void onPartArrived(PartArrivedEvent event) {
try {
Thread.sleep(triggerDelayMs); // 等零件稳定
byte[] imageData = camera.capture();
// 发送到图像处理队列
imageQueue.put(ImageTask.builder()
.partId(event.getPartId())
.productLine(event.getProductLine())
.imageData(imageData)
.captureTime(Instant.now())
.build());
} catch (Exception e) {
log.error("拍照失败,零件ID:{}", event.getPartId(), e);
alertService.sendAlert(AlertLevel.HIGH, "质检拍照失败:" + event.getPartId());
}
}
}图像预处理用 OpenCV(Java绑定)做:
@Component
public class ImagePreprocessor {
public Mat preprocess(byte[] rawImageData, ProductLineConfig config) {
// 解码图像
Mat src = Imgcodecs.imdecode(
new MatOfByte(rawImageData),
Imgcodecs.IMREAD_GRAYSCALE
);
// 1. 去噪
Mat denoised = new Mat();
Photo.fastNlMeansDenoising(src, denoised, 3, 7, 21);
// 2. 直方图均衡化(提升对比度)
Mat equalized = new Mat();
Imgproc.equalizeHist(denoised, equalized);
// 3. ROI裁剪(只看关键检测区域)
if (config.getRoi() != null) {
Rect roi = new Rect(
config.getRoi().getX(),
config.getRoi().getY(),
config.getRoi().getWidth(),
config.getRoi().getHeight()
);
equalized = new Mat(equalized, roi);
}
// 4. 尺寸归一化
Mat resized = new Mat();
Imgproc.resize(equalized, resized, new Size(640, 640));
return resized;
}
}预处理这步非常关键。没做预处理之前,同一个零件在不同光照下,模型给出的置信度差异能有20%以上。做了之后稳定多了。
两阶段检测策略
这是整个系统最核心的设计思路,值得详细说。
第一阶段:轻量检测模型(YOLO v8)
用来快速判断"这个零件有没有异常区域",速度要快,必须跟上产线节拍(我们的产线是每分钟180个零件,也就是说每个零件留给检测的时间不到333ms)。
@Service
public class DefectDetectionService {
// YOLO模型通过ONNX Runtime加载
private OrtEnvironment onnxEnv;
private OrtSession detectionSession;
@PostConstruct
public void loadModel() throws OrtException {
onnxEnv = OrtEnvironment.getEnvironment();
OrtSession.SessionOptions opts = new OrtSession.SessionOptions();
opts.addCPUProvider(0);
// 如果有GPU,可以换成addCUDAProvider
detectionSession = onnxEnv.createSession(
modelPath, opts
);
}
public DetectionResult quickDetect(Mat processedImage) {
long startTime = System.currentTimeMillis();
// 图像转为模型输入格式
float[] inputData = matToFloat(processedImage);
OnnxTensor inputTensor = OnnxTensor.createTensor(
onnxEnv,
FloatBuffer.wrap(inputData),
new long[]{1, 3, 640, 640}
);
Map<String, OnnxTensor> inputs = Map.of("images", inputTensor);
OrtSession.Result output = detectionSession.run(inputs);
List<DefectBox> boxes = parseYoloOutput(output);
long elapsed = System.currentTimeMillis() - startTime;
log.debug("YOLO检测耗时:{}ms,检测到{}个候选缺陷区域", elapsed, boxes.size());
return DetectionResult.builder()
.hasDefect(!boxes.isEmpty())
.defectBoxes(boxes)
.detectionTimeMs(elapsed)
.build();
}
}实际测试下来,YOLO推理时间在80-120ms之间(CPU推理),满足要求。
第二阶段:视觉大模型精细分析
只有第一阶段判断"有问题"的零件才进到这里,做精细的缺陷分类和严重程度判断。
这里用了 Spring AI 接入视觉模型(我们用的是私有化部署的 LLaVA,不过接口和 OpenAI 兼容,所以代码基本一样):
@Service
public class VisionAnalysisService {
@Autowired
private ChatClient chatClient;
public DefectAnalysis analyzeDefect(
byte[] imageData,
List<DefectBox> suspectedAreas,
ProductLineConfig config) {
// 把图像编码为base64
String base64Image = Base64.getEncoder().encodeToString(imageData);
// 构建包含质检标准的Prompt
String analysisPrompt = buildAnalysisPrompt(suspectedAreas, config);
ChatResponse response = chatClient.prompt()
.user(u -> u
.text(analysisPrompt)
.media(MimeTypeUtils.IMAGE_JPEG,
new ByteArrayResource(imageData))
)
.call()
.chatResponse();
String analysisText = response.getResult().getOutput().getContent();
return parseDefectAnalysis(analysisText);
}
private String buildAnalysisPrompt(
List<DefectBox> areas,
ProductLineConfig config) {
StringBuilder sb = new StringBuilder();
sb.append("你是一位精密零件质检专家,请分析图片中的零件缺陷情况。\n\n");
sb.append("【质检标准】\n");
sb.append("产品线:").append(config.getProductLineName()).append("\n");
sb.append("允许缺陷级别:\n");
for (DefectStandard standard : config.getDefectStandards()) {
sb.append("- ").append(standard.getDefectType())
.append(":允许").append(standard.getMaxSeverity()).append("级以下\n");
}
if (!areas.isEmpty()) {
sb.append("\n【初步检测到的可疑区域】\n");
for (int i = 0; i < areas.size(); i++) {
DefectBox box = areas.get(i);
sb.append(String.format("区域%d:位置(x=%d,y=%d,w=%d,h=%d),"
+ "初判类型:%s,置信度:%.2f\n",
i+1, box.getX(), box.getY(),
box.getWidth(), box.getHeight(),
box.getDefectType(), box.getConfidence()));
}
}
sb.append("\n请判断:\n");
sb.append("1. 每个可疑区域的实际缺陷类型(划痕/毛刺/氧化/形变/其他)\n");
sb.append("2. 缺陷严重程度(1-5级,5最严重)\n");
sb.append("3. 是否符合该产品线质检标准\n");
sb.append("4. 综合判定:合格/轻微不合格(建议人工复核)/严重不合格(直接剔除)\n");
sb.append("\n以JSON格式返回,字段:defects(数组)、overallResult、confidence、remark。");
return sb.toString();
}
}这里有个性能问题需要注意:视觉大模型的推理时间在2-5秒,远超产线节拍。所以视觉大模型分析必须异步进行,不能阻塞产线。
我们的方案是:YOLO判断有缺陷的零件,先放到"疑似不合格区"物理暂存,同时提交异步分析任务。等视觉模型分析完,再决定是进合格品仓还是报废。这段等待时间通常不超过30秒,完全可以接受。
人工复核与反馈学习
系统上线初期,我强烈建议保留人工复核环节。原因很简单:模型不是万能的,需要人来兜底,同时人工复核的数据可以用来持续改进模型。
@Service
public class ReviewFeedbackService {
@Autowired
private ModelRetrainingQueue retrainingQueue;
/**
* 质检员对AI判断给出复核结果
*/
public void submitReview(
String partId,
ReviewResult humanResult,
String reviewNote) {
QualityRecord record = qualityRecordRepo.findByPartId(partId)
.orElseThrow(() -> new PartNotFoundException(partId));
record.setHumanReviewResult(humanResult);
record.setReviewNote(reviewNote);
record.setReviewedAt(Instant.now());
qualityRecordRepo.save(record);
// 判断AI是否出错
boolean aiCorrect = record.getAiResult() == humanResult.toAiResultEnum();
if (!aiCorrect) {
// AI判断错误,记录为训练样本
TrainingSample sample = TrainingSample.builder()
.imageId(record.getImageId())
.aiLabel(record.getAiResult().name())
.humanLabel(humanResult.name())
.disagreementType(determineDisagreementType(
record.getAiResult(), humanResult))
.productLine(record.getProductLine())
.capturedAt(record.getCaptureTime())
.build();
retrainingQueue.add(sample);
log.info("AI判断与人工复核不一致:零件{},AI={},人工={}",
partId, record.getAiResult(), humanResult);
}
// 定期触发模型更新(积累100个不一致样本后)
if (retrainingQueue.size() >= 100) {
triggerModelUpdate();
}
}
private void triggerModelUpdate() {
// 发送模型更新任务到ML平台
mlPlatformClient.submitRetrainingJob(
RetrainingJob.builder()
.modelType(ModelType.DEFECT_DETECTION)
.newSamples(retrainingQueue.drainToList())
.baseModelVersion(currentModelVersion)
.build()
);
}
}模型持续学习这块我想多说几句。在工厂场景里,数据分布会随时间变化:工具磨损后产生的划痕特征会变化,换了批原材料后零件表面纹理也会变化。如果模型不更新,半年后准确率就会掉下来。
我们设置的策略是:每天统计人工复核的不一致率,如果连续三天超过3%,自动触发模型更新评估。
漏检率和误检率的权衡
回到最开始说的难题:漏检率0.1%和误检率5%如何同时满足?
关键在于置信度阈值的配置化。
对于高危缺陷(比如会导致产品安全问题的结构性缺陷),我们把置信度阈值设得很低,宁可误检也不漏检:
@Component
public class DefectThresholdConfig {
// 缺陷类型 -> 判定阈值配置
private static final Map<String, ThresholdConfig> THRESHOLDS = Map.of(
"structural_crack", new ThresholdConfig(0.3, AlertLevel.CRITICAL),
"dimensional_error", new ThresholdConfig(0.5, AlertLevel.HIGH),
"surface_scratch", new ThresholdConfig(0.7, AlertLevel.MEDIUM),
"minor_burr", new ThresholdConfig(0.85, AlertLevel.LOW)
);
public boolean shouldFlag(String defectType, double confidence) {
ThresholdConfig config = THRESHOLDS.getOrDefault(
defectType,
new ThresholdConfig(0.6, AlertLevel.MEDIUM)
);
return confidence >= config.getThreshold();
}
public boolean requiresImmediateReject(String defectType, double confidence) {
ThresholdConfig config = THRESHOLDS.get(defectType);
return config != null
&& config.getAlertLevel() == AlertLevel.CRITICAL
&& confidence >= 0.6;
}
}结构性裂纹这种缺陷,置信度只要到30%就报警,因为漏检代价极高(可能导致安全事故)。
表面轻微划痕,要到85%才报,因为客户对这个容忍度高,过多误检会让质检员失去信任感。
上线六个月后的统计数据:
- 漏检率:0.07%(满足0.1%要求)
- 误检率:4.2%(满足5%要求)
- 质检效率:从人均每小时检300个提升到每小时检1800个(人工只需要处理有疑问的)
- 质检员从12人缩减到3人(其中2人专门做复核,1人做设备维护)
工程上的一些经验
1. 工业现场网络要特别处理
工厂里的网络环境很差,有时候大模型API调用会超时。必须做重试和降级:
@Configuration
public class VisionClientConfig {
@Bean
public RestTemplate visionRestTemplate() {
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(3000);
factory.setReadTimeout(10000); // 视觉模型需要更长时间
return new RestTemplateBuilder()
.requestFactory(() -> factory)
.additionalInterceptors(new RetryInterceptor(3)) // 重试3次
.build();
}
}大模型API不可用时的降级策略:只用YOLO的结果,但把置信度阈值降低(更保守),同时增加人工复核比例。
2. 图像存储成本
产线每天产生几十万张图像,全存是不现实的。我们的策略:
- 合格品图像保留7天后删除
- 有缺陷的图像永久保留(用于训练)
- 人工复核过的图像加标注后永久保留
3. 模型版本管理
生产环境不能随意更新模型,每次模型更新都要经过:离线评估 → 小批量灰度(10%流量) → 全量上线。严格走这个流程,避免了两次因为模型更新导致的漏检率上升。
