资讯动态

Java+YOLOv5食品包装缺陷检测实战:从模型导出到Spring Boot API对接

发布时间:2026/10/19 17:44:00 来源:尧图企业网站定制
JavaYOLOv5食品包装缺陷检测实战从模型导出到Spring Boot API对接前几个月在做一个食品产线的质检项目需求很朴素检测包装袋的封口是否平整、有无漏气皱褶、印刷字符是否错位、包装表面有没有污渍。算法那边用YOLOv5训练了一版模型效果不错但到了部署环节突然发现一个尴尬问题——产线管理系统、MES对接层、设备控制服务全是Java写的算法同学丢过来一个.pt权重文件就撤了。剩下的事情就是把YOLOv5模型真正跑进Java服务里再包一层Spring Boot API给产线调用。这中间的水比想象中深。模型导出踩了坑、Java端图像预处理和Python端不一致导致检测结果对不上、ONNX Runtime集成后内存暴涨、API并发一上来就超时……前前后后折腾了两周把整套链路跑通之后回看其实核心问题就那几个模型怎么导、Java端怎么接、前后处理怎么写、服务怎么扛住产线的并发。这篇文章就把这套完整方案拆开讲从模型导出到Spring Boot API对接每一步给出可直接照抄的做法和参数顺手把我踩过的坑一并交代清楚。这篇文章适合两类人看一是Java后端工程师被分配了“把模型接进服务”的活PyTorch不熟但想把事情干成二是算法工程师训练完模型之后想自己把部署链路打通不用每次都在Python里起一个服务给Java那边调用。两边视角我都尽量覆盖。1. 方案设计先想清楚部署链路再动手1.1 缺陷检测场景的真实约束食品包装的缺陷检测和通用目标检测不太一样。产线上是固定工位的工业相机拍照距离基本固定光照环境相对可控被检物体包装袋的形态比较规整。这意味着两件事第一模型不需要做旋转目标检测也不需要多尺度强鲁棒性常规YOLOv5已经够用第二推理时延是硬指标产线节拍如果是一分钟60包那么单包检测时间必须控制在1秒以内才能跟上流水线速度否则就得降速或者增加并行工位。另外缺陷类别通常是小目标。比如封口处的皱褶在1080P图像里可能只占几十个像素包装袋角落的污渍面积也不大。所以训练时输入尺寸不能压得太低我用的是640x640输入特征层采样的信息足够保留小目标细节。这个选择会在Java端的预处理环节直接产生影响后面细说。还有一个很容易被忽视的点缺陷检测的“误报代价”很高。食品行业如果放过了一个漏气袋到了流通环节就会变成客诉但反过来如果误报率太高产线工人要频繁处理假阳性慢慢就不信任这套系统了。所以后处理时的置信度阈值不能只定一个我最后在API层做了分缺陷类别的阈值配置而不是在模型里写死方便现场调参。1.2 技术选型为什么用ONNX Runtime而不是直接用PyTorch先说一下我当时筛选部署方案的思路。算法同学给的模型是PyTorch训练的.pt文件Java端有几条路可以走在Python侧单独部署一个PyTorch推理服务Java通过HTTP调用。缺点很明显多维护一个进程跨语言网络调用有传输开销而且Python服务的稳定性不如JVM体系好保障。产线环境里不能接受一个三天两头被内存杀掉的Python进程当核心依赖。用Java端的PyTorch官方库。PyTorch Java API覆盖率一直不太行当时试了一下部分算子支持不完整而且.pt文件里包含Python对象序列化信息Java端加载对版本极度敏感踩坑成本太高。导出ONNX模型用ONNX Runtime Java API加载推理。这是当时综合评估下来最稳的方案也是我现在给类似项目做技术选型时的默认答案。ONNX是模型交换的标准格式ONNX Runtime在Java侧的API虽然不算丰富但推理这核心功能非常稳定CPU和GPU都能跑和Spring Boot集成没有任何额外负担。另外ONNX Runtime还有一个好处是对CPU推理做了深度优化。产线现场很多工控机是没有独立显卡的纯CPU推理能跑到什么性能直接决定方案能不能落地。YOLOv5s模型导出为ONNX后在普通i7工控机上640x640输入大约可以做到80~120ms一帧去掉前后处理的时间整体足以满足秒级节拍。方案架构一句话总结YOLOv5 训练产出.pt→ 导出带NMS后处理的ONNX模型 → Spring Boot 服务通过 ONNX Runtime 加载模型 → 接收产线相机上传的图片 → 预处理 → 推理 → 后处理 → 返回缺陷坐标、类别和置信度 → 上位机/控制系统根据结果做剔除动作。2. 模型导出与转换从.pt到ONNX的关键操作2.1 导出前的检查和准备先用Python环境把YOLOv5跑通确认PyTorch版本。这里有个非常关键的前提训练环境和导出环境的PyTorch版本要尽量接近否则导出时有些算子映射会不一致。我踩过一次坑训练在PyTorch 1.12导出机器上是2.0导出的ONNX在Java端加载报“Unsupported operator”错后来重新在同等版本环境导出就好了。导出前还要确认模型是否已经转为半精度或做了剪枝。常规做法是训练完保留best.pt如果追求CPU推理速度可以尝试导出FP16模型。但是FP16在CPU上的优化收益有限反而可能在边缘设备上出现精度下降我的建议是先用FP32导出跑通链路再考虑FP16。导出的核心命令如下建议直接写在requirements.txt环境固定版本python export.py --weights best.pt --include onnx --opset 12 --simplify三个参数背后是有讲究的--include onnx只导出ONNX不要顺手导出TorchScript后者Java端用不上。--opset 12ONNX算子集版本。ONNX Runtime对opset 12的支持非常成熟再高的版本比如15、17虽然新特性多但在某些老版本ONNX Runtime上会遇到算子缺失的问题。低版本opset兼容性最好除非模型里用到了新算子否则就用默认的 12 或根据YOLOv5默认值来。--simplify用onnx-simplifier做模型精简去掉冗余算子比如Shape、Gather等无关计算这一步能让模型体积减少5%~10%推理速度也能提升一点。导出后务必看一眼模型大小。YOLOv5s大概14MB左右如果导出后是40MB多半是导出了整个训练模型包含优化器状态需要重新用export.py脚本导出。模型大小直接决定Java端加载时间和内存占用不能忽略。2.2 导出带NMS和不带NMS的差异YOLOv5的export.py默认导出的ONNX是不带NMS后处理的模型的输出是原始预测张量形状为[1, 25200, 85]以COCO 80类为例25200 3个尺度 x (640/8)^2 (640/16)^2 (640/32)^285 5 80即cx, cy, w, h, objectness, class_scores。这意味着后处理坐标解码、置信度过滤、NMS去重必须在Java端手动完成。也有人在导出时就通过--end-to-end参数把NMS集成到ONNX模型里这样Java端就不用写NMS代码直接拿最终检测框。我强烈建议不要在导出时集成NMS原因有三第一集成NMS的ONNX模型在不同ONNX Runtime版本上的算子兼容性差异很大经常出现 “NonMaxSuppression” 算子不支持的情况。第二内置NMS会把置信度阈值和IoU阈值固化在模型里现场想调阈值就要重新导出模型非常不方便。第三Java端手写NMS并不困难百来行代码而已还能顺便把一些业务逻辑比如按缺陷类型配置阈值合进去灵活性高得多。所以最终目标是导出不带NMS的原始ONNXJava端自己处理后处理。导出后不要急着写Java先用Python做一次“对齐验证”这是整个部署链路中最容易省掉但最不能省的一步import onnxruntime as ort import numpy as np import cv2 sess ort.InferenceSession(best.onnx, providers[CPUExecutionProvider]) img cv2.imread(test.jpg) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (640, 640)) img img.astype(np.float32) / 255.0 img img.transpose(2, 0, 1) img np.expand_dims(img, 0) outputs sess.run(None, {sess.get_inputs()[0].name: img}) print(outputs[0].shape)这一步的意义在于先确认模型在Python侧输出是正常的再对比Java端输出。如果Python侧结果都是乱的说明模型本身有问题Java端写得再好也没用。对齐验证时固定一张测试图把Python侧的结果坐标、类别、置信度记下来后面Java端部署完用同一张图跑结果应该完全一致。3. Java环境搭建与图片预处理最容易出问题的环节3.1 Maven依赖和模型目录规划Java工程引入ONNX Runtime非常简单Maven里加一行依赖dependency groupIdcom.microsoft.onnxruntime/groupId artifactIdonnxruntime/artifactId version1.16.3/version /dependency版本我建议用当前最新的稳定版不要用太老的版本因为ONNX Runtime每个版本对算子支持和CPU性能优化都在持续改进。实际项目里我用的1.16.3稳定跑了很久。模型文件放哪里也值得提前约定。不要直接把.onnx文件塞进Spring Boot的resources目录里打包进Jar因为产线环境经常需要热更新模型重新训练后替换权重每次更新都要重新打Jar再发布太痛苦了。正确做法是放在外部目录通过配置文件指定路径detect: model-path: /data/models/best.onnx input-size: 640 conf-threshold: 0.25 iou-threshold: 0.45启动时通过Value注入路径服务初始化时加载模型。这样后续更新模型只需覆盖文件然后调用一个/admin/reload接口触发重新加载即可不需要重启服务。3.2 图像预处理的每个步骤都必须和Python端完全一致前面给Python对齐用的预处理是BGR转RGB→resize到640x640→归一化到0~1→NCHW排列。Java端要做到一模一样但实际写起来有两个隐藏的坑。第一个坑是resize方式。YOLOv5默认的letterbox是等比缩放加灰度填充pad不是直接把图拉伸到640x640。如果直接用BufferedImage.getScaledInstance(640, 640)把图硬拽变形检测精度会明显下降因为目标的长宽比被破坏了。实际上YOLOv5 export后的模型期望的输入是“已letterbox”的图像原因很简单模型训练时也是这么做的。所以在Java端需要自己实现letterbox逻辑public static int[] letterbox(BufferedImage src, int targetSize) { int srcW src.getWidth(); int srcH src.getHeight(); double scale Math.min((double) targetSize / srcW, (double) targetSize / srcH); int newW (int) Math.round(srcW * scale); int newH (int) Math.round(srcH * scale); int padX (targetSize - newW) / 2; int padY (targetSize - newH) / 2; BufferedImage resized new BufferedImage(newW, newH, BufferedImage.TYPE_3BYTE_BGR); Graphics2D g resized.createGraphics(); g.drawImage(src, 0, 0, newW, newH, null); g.dispose(); BufferedImage canvas new BufferedImage(targetSize, targetSize, BufferedImage.TYPE_3BYTE_BGR); Graphics2D g2 canvas.createGraphics(); g2.setColor(Color.GRAY); // YOLOv5默认用灰值114填充 g2.fillRect(0, 0, targetSize, targetSize); g2.drawImage(resized, padX, padY, null); g2.dispose(); return new int[]{padX, padY, scale}; }注意YOLOv5源码letterbox的填充色是(114, 114, 114)不是纯黑也不是纯白。这个细节必须对齐否则图像分区填充区域的像素值会影响输入分布降低检测精度。第二个坑是内存中的像素排列。Java的BufferedImage像素存储是CHW之外的交错格式说白了就是每个像素的B、G、R三个字节连续排列。而ONNX Runtime需要的输入是NCHW也就是先所有通道的Row0再所有通道的Row1。转换时要手动把像素按通道拆开重排float[] input new float[3 * targetSize * targetSize]; for (int y 0; y targetSize; y) { for (int x 0; x targetSize; x) { int rgb canvas.getRGB(x, y); int b rgb 0xFF; int g (rgb 8) 0xFF; int r (rgb 16) 0xFF; int idx y * targetSize x; input[idx] r / 255.0f; input[idx targetSize * targetSize] g / 255.0f; input[idx 2 * targetSize * targetSize] b / 255.0f; } }getRGB每像素调用在性能上是灾难性的一张640x640的图要执行40万次还要做位运算实测下来预处理耗时能到300~500ms完全不可接受。优化方案是直接拿像素数组批量读取int[] pixels canvas.getRGB(0, 0, targetSize, targetSize, null, 0, targetSize); for (int i 0; i pixels.length; i) { int rgb pixels[i]; int b rgb 0xFF; int g (rgb 8) 0xFF; int r (rgb 16) 0xFF; float val (r 0xFF) / 255.0f; input[i] val; input[i targetSize * targetSize] g / 255.0f; input[i 2 * targetSize * targetSize] b / 255.0f; }这样一次getRGB拷贝整个像素数组40万次位运算在毫秒级完成。实测预处理转换总耗时可以压到30ms以内。关于BGR和RGB的顺序如果模型训练时走的是标准YOLOv5默认方式图片是先转RGB再进入网络的那Java端也要转。但要注意如果训练时喂的本来就是OpenCV读出来的BGR图那导出后模型输入的通道顺序其实是BGR。这必须回到训练脚本里去确认。我当时就是没确认清楚Java端转了RGB之后检测精度比Python侧低了一截检查了一天发现训练脚本里压根没有cv2.cvtColor这一步模型期望的是BGR输入。所以提前记住完全以训练时的数据管线为准不是所有YOLOv5模型都要转RGB。3.3 模型加载和Session配置ONNX Runtime在Java端的使用方式如下import ai.onnxruntime.*; OrtEnvironment env OrtEnvironment.getEnvironment(); OrtSession.SessionOptions options new OrtSession.SessionOptions(); options.setOptimizationLevel(OrtSession.SessionOptions.OptLevel.ALL_OPT); options.setIntraOpNumThreads(4); OrtSession session env.createSession(modelPath, options);这个session是线程安全的多个线程可以同时调用session.run()做推理不需要加锁也不需要为每个线程单独创建session。这是ONNX Runtime设计好的并发模型不要每来一张图就重新createSessionsession创建过程中要解析模型结构、申请内存非常耗时。内部线程数setIntraOpNumThreads需要根据CPU核心数调整。不是线程越多越好ONNX Runtime内部算子并行度太高会导致线程频繁切换反而变慢。在8核工控机上实测4~6个内部线程综合效果最好在只分配了2核的容器里就把线程数设成2。如果服务本身还承担了其他业务建议先用4核跑观察CPU占用再慢慢调。GPU模式下还需要设置CUDA provider代码会复杂一点。如果产线现场有显卡配置方式是在创建session时添加CUDA provideroptions.addCUDA(0);同时把依赖里增加onnxruntime-gpu替代原来的onnxruntime。但GPU不是必须的我们项目最后反而是CPU方案更省心现场工控机显卡驱动成了新的故障点CPU推理跑下来也能满足节拍要求。部署环境的稳定性优先于推理速度。4. 核心推理与后处理坐标解码和NMS4.1 推理调用ONNX Runtime的输入是一个OnnxTensor可以直接把上面算好的float[]包进去long[] shape new long[]{1, 3, 640, 640}; OnnxTensor inputTensor OnnxTensor.createTensor(env, FloatBuffer.wrap(inputData), shape); MapString, OnnxTensor inputs new HashMap(); inputs.put(session.getInputNames().iterator().next(), inputTensor); try (OnnxTensor output (OnnxTensor) session.run(inputs).get(0)) { float[][][] result (float[][][]) output.getValue(); // result[0] 形状是 [1, 25200, 85] // 每一行是 cx, cy, w, h, objectness, class_scores... }注意session.getInputNames()返回的是Set虽然只有一个输入但要用迭代器拿到不要硬编码字符串避免模型重导出后输入节点名变化导致报错。4.2 输出张量解析从中心坐标到边界框ONNX输出的是YOLOv5原始格式每个预测框用(cx, cy, w, h)表示中心点坐标和宽高后面是objectness该框内是否有目标的置信度和各类别得分。要先把它转换成(x1, y1, x2, y2)的左上右下坐标float cx row[0], cy row[1], w row[2], h row[3]; float objScore row[4]; float x1 cx - w / 2; float y1 cy - h / 2; float x2 cx w / 2; float y2 cy h / 2; // 得分 objectness * classScore float maxClassScore 0; int maxClassId -1; for (int c 5; c row.length; c) { float clsScore row[c]; float score objScore * clsScore; if (score maxClassScore) { maxClassScore score; maxClassId c - 5; } }这里用的是“目标置信度 x 类别置信度”的乘积作为最终得分这是YOLOv5的输出逻辑。只取classScore不问objectness是不对的会在背景区域产生大量虚警。另外因为输入做了letterbox检测出来的坐标是相对于640x640输入图的。要还原到原始图像坐标需要把前面letterbox记录的padX, padY, scale反向换算float origX1 (x1 - padX) / scale; float origY1 (y1 - padY) / scale; float origX2 (x2 - padX) / scale; float origY2 (y2 - padY) / scale; // 需要做边界裁剪防止坐标越界 // origX1 Math.max(0, origX1); ...没有做反向映射就直接把框画到原图上位置会整体偏移。训练和推理之间的每一个对齐细节最后都体现在这一个坐标偏移上。4.3 NMS的Java实现之后需要对过滤后的框做NMS去重。同一缺陷可能会被多个尺度的anchor同时检出NMS保留置信度最高、抑制IoU过高的重叠框。NMS的Java实现不复杂但有几个细节要注意。先用置信度阈值过滤掉低分框float confThreshold 0.25f; // 从配置读取 ListDetection detections new ArrayList(); for (float[] row : result[0]) { float score ...; // 上面算出的最终得分 if (score confThreshold) { detections.add(new Detection(x1, y1, x2, y2, score, classId)); } }然后按得分从高到低排序逐个取框把和当前框IoU超过阈值的框都删掉detections.sort((a, b) - Float.compare(b.score, a.score)); ListDetection picked new ArrayList(); while (!detections.isEmpty()) { Detection best detections.remove(0); picked.add(best); IteratorDetection iter detections.iterator(); while (iter.hasNext()) { Detection d iter.next(); if (calcIoU(best, d) iouThreshold) { iter.remove(); } } }IoU计算是矩形面积比的经典公式注意并集面积要减去交集面积避免重叠部分被重复计算。IoU阈值一般取0.45~0.5过低会导致同一个目标被拆成多个框过高则无法有效去重现场可以调。不过NMS有个常见问题当两个不同类别的缺陷目标高度重叠时会被NMS合并掉一个。比如包装袋上一张污渍标签恰好叠在印刷字符错位上如果两个目标框IoU超过阈值得分低的那个会被抑制。解决方法是按类别分组做NMS同一类别之间做NMS不同类别不抑制。这样不会漏掉叠加缺陷。5. Spring Boot API封装与工程化5.1 接口设计和返回结构API的思路是接收前端/上位机传来的图片返回检测到的缺陷列表。接口路径建议设计成两个一个给产线实时检测用另一个给算法调试用POST /api/detect传图片二进制流返回缺陷检测结果GET /api/model/info返回当前模型信息、各类别名称、阈值配置方便前端做展示返回结构要考虑到调用方的消费能力。产线控制系统只要知道“这包有没有问题有问题就触发剔除”所以JSON不要设计太复杂{ code: 0, message: success, data: { defectCount: 2, defects: [ { type: wrinkle, typeName: 封口皱褶, confidence: 0.93, x1: 112.5, y1: 340.2, x2: 178.3, y2: 402.9 } ], elapsedMs: 96 } }elapsedMs这个字段一定要返回现场调试时根据它判断系统的性能余量。我那时就是靠这个字段发现某台工控机CPU占用过高、推理时长翻倍的问题。5.2 异步调用与线程池配置产线调用方比如PLC或上位机的请求特征是高频率、单张图片数据量不大、对响应时间敏感。如果Spring Boot默认的Tomcat线程池来处理每个请求占用一个线程做推理那么顶层配置的线程数就是系统的最大并发处理能力。推理属于CPU密集操作。8核机器上如果线程数设为8每个线程都在抢CPU时间片上下文切换开销很大。我自己的经验是CPU密集型推理服务的线程数 CPU核心数或核心数-1预留一个核心给GC和网络IO处理。可以这样配置Bean(detectExecutor) public ThreadPoolTaskExecutor detectExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(4); executor.setQueueCapacity(200); executor.setThreadNamePrefix(detect-); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); return executor; }CallerRunsPolicy被拒绝的任务由调用线程执行而不是直接抛异常。在产线并发高峰时这相当于一种保险机制不会因为线程池满了就丢弃检测请求代价是调用方线程会被占用在推理上响应变慢。对于产线系统丢一个检测请求意味着漏检一个产品远比变慢严重。接口内部的处理逻辑应该是请求进来 → 图片解码 → 预处理 → 提交给线程池推理 → 同步等待结果。用CompletableFuture做异步编排PostMapping(/detect) public DetectResponse detect(RequestParam(file) MultipartFile file) { // 提交推理任务到线程池 CompletableFutureDetectResult future CompletableFuture.supplyAsync( () - detectService.detectImage(file), detectExecutor); DetectResult result future.get(5, TimeUnit.SECONDS); // 超时保护 return DetectResponse.success(result); }超时保护非常重要。万一模型推理卡死ONNX Runtime偶发的线程饥饿问题不能让产线请求无限等下去。5秒超时是在产线允许范围内单包节拍1秒但如果整体卡住给5秒重试窗口是合理的的兜底方案。5.3 图片解码和跨域问题MultipartFile是个松耦合接口但实际操作时要先把文件字节转成BufferedImage。这一步常见问题是相机上传的图片格式可能不止JPEG有些工业相机直接输出BMP甚至裸RGB数据。我建议服务端统一用ImageIO.read(new ByteArrayInputStream(bytes))它对JPEG、PNG、BMP都支持。但如果遇到ImageIO不支持的格式就要考虑加上OpenCV的Java绑定。跨域配置是另一个现场常遇到的问题。前端监控大屏页面和检测服务往往是分开部署的直接调用API会触发CORS拦截。在配置类里加一个WebMvcConfigurer统一定义放行规则Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST) .maxAge(3600); } }这里有个细节allowedOrigins(*)在Spring Boot新版里不允许和allowCredentials(true)同时使用会启动报错。产线检测接口不需要cookie所以直接不要开credentials或者用allowedOriginPatterns(*)前者版本兼容写法。6. 生产环境踩坑记录问题排查速查表6.1 高频问题整理按下表整理是我调试期内遇到的高频问题这些问题网上零散也有但集中成表方便在产线现场快速对照问题现象可能原因排查思路与解决Java推理结果和Python不一致预处理链路不一致resize方式、通道顺序、归一化固定同一张测试图分别打点比对预处理结果坐标偏移但能框出目标letterbox的padX/padY/scale未反向映射检查后处理反向坐标计算的逻辑session.run()抛OOM并发请求过多tensor内存未释放使用try-with-resources管理OnnxTensor检查每个请求是否关闭了tensor检测结果时有时无图片格式不统一解码失败后降级处理增加格式校验兼容工业相机裸输出推理耗时突然变长线程池被阻塞请求排队等待监控线程池活跃数和队列长度排查是否有长任务耗尽线程加载模型时提示unsupported operator训练/导出环境opset版本过高或过低固定opset12重新导出更新ONNX Runtime版本6.2 隐蔽故障实录ONNX Runtime的Tensor释放问题ONNX Runtime的Java API有个隐蔽的坑**OrtSession.run()返回的结果必须在用完后关闭否则DirectByteBuffer的内存不会被释放**。Java堆内存外还有一块直接内存JVM的Xmx参数管不到它如果服务跑几天内存持续上涨最终会OutOfMemory。正确做法是使用try-with-resourcestry (OrtSession.Result result session.run(inputs)) { OnnxTensor output (OnnxTensor) result.get(0); // 处理结果... }同时在每个请求结束时也要把输入tensor关闭try (OnnxTensor inputTensor OnnxTensor.createTensor(...)) { // 推理逻辑 }这个坑在本地压测短时间看不出来跑到第几百个小时就暴露了。我们当时线上服务跑了3天内存翻倍排查了很久最后发现是session.run返回值没有关闭导致的。6.3 模型热更新机制产线系统不能频繁重启。模型重新训练后在服务运行状态下把权重文件替换掉再触发一个reload接口即可。实现方式是维护一个volatile OrtSession引用reload时创建新session再替换旧引用private volatile OrtSession session; public synchronized void reloadModel(String path) throws OrtException { OrtSession newSession env.createSession(path, options); OrtSession oldSession this.session; this.session newSession; if (oldSession ! null) { oldSession.close(); } }用synchronized保证reload不会和推理请求并发创建/关闭session。旧session等新session加载完成后再关闭避免出现“正在推理时模型被卸载”的竞态问题。这个机制在现场帮了大忙算法那边迭代了一版新模型直接在中午换班间隙调接口热切换完全没影响产线。最后的实践经验小结这套方案从模型导出到Spring Boot API对接核心难点其实不在模型本身而在各种链路对齐的细节上预处理顺序对不对、letterbox有没有做、坐标有没有反向换算、tensor有没有释放。把这些对齐了整套系统跑起来非常稳。我在这个项目里最后沉淀的几条经验是第一部署之前一定要做Python和Java的对齐验证这能省掉后面90%的排查时间第二所有阈值参数全部配置化不要在代码里写死现场调优时你会感谢这个决定第三ONNX Runtime的session是线程安全的但tensor不是该释放的一定要释放。如果你正在做类似的项目希望这篇文章能让你少走几段弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑