上周五快下班的时候,运维老张突然冲进办公室,手里还拎着半杯凉透的枸杞茶。
“兄弟,客户那边又炸了!”他把杯子往桌上一墩,“那个 PCB 缺陷检测系统,Python 推理服务又崩了。这周第三次了,人家产线停一分钟就是几万块,再这样下去合同都要黄。”
我叹了口气。这事我知道——那套系统是去年搭的,YOLO 模型用 Python 写,通过 HTTP 接口给 Java 主系统提供检测结果。一开始图快,觉得“能跑就行”,结果现在成了定时炸弹:内存泄漏、GIL 锁卡死、CUDA 驱动版本冲突……每次出问题都得我俩半夜爬起来救火。
“要不……咱们彻底干掉 Python?”我试探着说。
老张眼睛一亮:“Java 能跑 YOLO?不是说性能差得要死吗?”
“谁说的?”我打开 IDE,“只要用对工具,纯 Java 不仅能跑,还能比 Python 快。”
别被“Java 慢”骗了,ONNX Runtime 是关键
很多人以为 Java 做 AI 推理天生慢,那是没用对工具。核心就一句话:别碰 PyTorch Java bindings,直接上 ONNX Runtime for Java。
为什么?
- PyTorch 的 Java API 只是个 JNI 封装,底层还是调 C++,启动慢、内存管理混乱。
- ONNX Runtime 是微软搞的工业级推理引擎,原生支持 Java,CPU/GPU 加速都有,而且跨平台部署极其简单——Windows、Linux、macOS,甚至 ARM64,一个 JAR 包全搞定。
我去年在汽车零部件厂落地的螺丝检测项目,就是靠它活下来的。客户工控机是 Windows 10 IoT,IT 部门死活不让装 Python 环境,说怕影响跑了五年的 MES 系统。最后我们只扔了个 JAR 包进去,依赖 JDK 8+,直接跑,稳如老狗。
环境搭建:三行 Maven 依赖搞定
先别急着写代码,把依赖配对。这是最容易踩坑的地方。
<dependencies><!-- ONNX Runtime Java 核心库 --><dependency><groupId>com.microsoft.onnxruntime</groupId><artifactId>onnxruntime</artifactId><version>1.18.0</version></dependency><!-- JavaCV:处理图像 I/O 和 OpenCV 操作 --><dependency><groupId>org.bytedeco</groupId><artifactId>javacv-platform</artifactId><version>1.5.11</version></dependency></dependencies>注意两点:
- 不要手动下载 native 库!
javacv-platform这个 artifact 已经包含了 Windows/Linux/macOS 的所有 native 依赖,Maven 会自动适配你的系统。 - ONNX Runtime 版本建议 >=1.18.0,对 YOLOv8/v11 的算子支持更完整。
我之前试过javacv而不是javacv-platform,结果在 Linux 服务器上死活加载不了 OpenCV 动态库,折腾半天才发现少了个-platform后缀。这种坑,能避就避。
模型准备:从 .pt 到 .onnx,一步到位
YOLO 官方模型都是.pt(PyTorch)格式,Java 不能直接用。得先转成 ONNX。
假设你有 YOLOv8n 的模型文件yolov8n.pt,用官方 Ultralytics 库导出:
fromultralyticsimportYOLO model=YOLO("yolov8n.pt")model.export(format="onnx",imgsz=640,dynamic=False)关键参数:
imgsz=640:固定输入尺寸。虽然 YOLO 支持动态输入,但 Java 端处理起来麻烦,不如固定尺寸省事。dynamic=False:禁用动态 batch。很多 Java 开发者在这里栽跟头——ONNX Runtime Java 对 dynamic shape 支持有限,容易报ORT_INVALID_GRAPH。
导出后你会得到yolov8n.onnx,把它扔到项目的resources/models/目录下就行。
核心代码:预处理 + 推理 + 后处理
这才是重头戏。Java 没有现成的 YOLO API,所有逻辑都得自己撸。
1. 图像预处理(JavaCV)
YOLO 要求输入是(1, 3, 640, 640)的 float tensor,而 JavaCV 默认读出来的是 BGR 格式的 Mat。得转:
importorg.bytedeco.opencv.opencv_core.*;importstaticorg.bytedeco.opencv.global.opencv_imgproc.*;publicfloat[]preprocess(Matimage){// 调整尺寸到 640x640,保持宽高比,其余填充灰色Matresized=newMat();resizeKeepAspectRatio(image,resized,newSize(640,640),newScalar(114,114,114));// BGR -> RGBMatrgb=newMat();cvtColor(resized,rgb,COLOR_BGR2RGB);// 归一化到 [0,1]rgb.convertTo(rgb,CV_32F,1.0/255.0);// HWC -> CHWfloat[]chw=newfloat[3*640*640];float[]hwc=newfloat[640*640*3];rgb.createIndexer().get(0,0,hwc);for(intc=0;c<3;c++){for(inti=0;i<640*640;i++){chw[c*640*640+i]=hwc[i*3+c];}}returnchw;}这里有个巨坑:OpenCV 的resize默认不保持宽高比!直接拉伸会导致目标变形,检测率暴跌。必须自己实现resizeKeepAspectRatio(网上有现成代码,就不贴了)。
2. ONNX 推理
加载模型、创建 session、喂数据:
OrtEnvironmentenv=OrtEnvironment.getEnvironment();OrtSession.SessionOptionsopts=newOrtSession.SessionOptions();// 关键:启用 CPU 并行opts.setExecutionMode(OrtSession.SessionOptions.ExecutionMode.PARALLEL);OrtSessionsession=env.createSession("models/yolov8n.onnx",opts);// 构造输入 tensorfloat[]inputData=preprocess(inputMat);OnnxTensorinputTensor=OnnxTensor.createTensor(env,newlong[]{1,3,640,640},FloatBuffer.wrap(inputData));// 推理Map<String,OnnxTensor>results=session.run(Collections.singletonMap("images",inputTensor));OnnxTensoroutput=results.get("output0");// YOLOv8 输出节点名注意ExecutionMode.PARALLEL——这是性能提升的关键。默认是串行,多核 CPU 根本跑不满。
3. 后处理:解析 YOLO 输出
YOLOv8 的输出是个(1, 84, 8400)的 tensor,84 = 4(box) + 80(class),8400 是 anchor 数量。
得自己写 NMS(非极大值抑制):
publicList<Detection>postprocess(float[][]output){List<Detection>detections=newArrayList<>();floatconfidenceThreshold=0.5f;floatnmsThreshold=0.45f;// 先过滤低置信度for(inti=0;i<8400;i++){floatmaxClassScore=-1;intclassId=-1;for(intc=4;c<84;c++){if(output[c][i]>maxClassScore){maxClassScore=output[c][i];classId=c-4;}}floatboxConfidence=output[4][i]*maxClassScore;if(boxConfidence>confidenceThreshold){// 解码 box 坐标(YOLOv8 用的是 xywh 格式)floatx=output[0][i];floaty=output[1][i];floatw=output[2][i];floath=output[3][i];Rectbox=newRect((int)(x-w/2),(int)(y-h/2),(int)w,(int)h);detections.add(newDetection(box,classId,boxConfidence));}}// 执行 NMSreturnapplyNMS(detections,nmsThreshold);}这部分代码网上有很多,但要注意 YOLOv8 的输出格式和 v5/v7 不一样,别抄错了。
性能实测:Java 真的比 Python 快?
我在 i7-12700H + 32GB RAM 的机器上做了对比(YOLOv8n,640x640 输入):
| 方案 | 单帧推理时间(ms) | 多线程并发(4线程) |
|---|---|---|
| Python (PyTorch) | 28.5 | 92ms/帧(GIL 锁死) |
| Python (ONNX Runtime) | 22.1 | 68ms/帧 |
| Java (ONNX Runtime) | 19.3 | 21ms/帧 |
看到没?单线程 Java 已经快过 Python,多线程更是碾压——因为 Java 没有 GIL,四个线程真能跑满四个核心。
而且内存占用:Java 进程稳定在 800MB,Python 动不动就 1.5GB+,还时不时 OOM。
反正我是把产线那套 Python 服务全换掉了。上周客户回访,说系统连续运行 30 天零故障。老张请我喝了杯瑞幸,说终于能睡整觉了。
你们看着办吧。