简介本资源面向需要在 Java 项目中落地视频目标检测的开发者提供一套 Java 调用 Python YOLO ONNX 模型的完整方案支持 YOLOv5、YOLOv7、YOLOv8 等主流模型并覆盖 RTSP/RTMP 视频流处理场景。整体架构由 Java 端负责视频流获取、预处理、数据传递、模型调用、后处理与结果展示Python 脚本负责加载 ONNX 模型执行推理并回传识别结果形成清晰的跨语言协作链路。压缩包共 32 个文件约 144.73MB包含 12 个 Java 源码、4 个 ONNX 模型、7 张效果图、2 个 dll 依赖以及 xml、jar、md 等配置与说明文件便于直接运行与二次开发。目前已有 506 人学习下载。读者可据此掌握 resize、normalization、padding 等预处理细节以及置信度过滤、识别框绘制等后处理思路快速搭建可复用的视频目标检测工程。1. Java 接 YOLO ONNX跨语言视频目标检测的落地路径很多做 Java 后端的同学遇到视频目标检测需求时第一反应是「Java 生态里深度学习支持太弱是不是得整个换 Python」。我一开始也这么想直到拆开yolo-onnx-java.zip这个包发现它给出的答案很务实Java 负责它最擅长的视频流获取、帧预处理和结果展示Python 只做一件事——加载 ONNX 模型跑推理两边通过进程调用把数据串起来。这套方案支持 YOLOv5、YOLOv7、YOLOv8 导出的 ONNX 模型覆盖 RTSP/RTMP 视频流场景适合已有 Java 服务、不想重构成微服务架构、又需要快速接入目标检测能力的团队。它不追求极致性能但胜在工程边界清晰、每一层都能单独调试对 Java 工程师来说上手成本远低于硬啃 DJL 或 TensorFlow Java。2. 架构拆解Java 与 Python 的职责边界怎么划2.1 为什么不让 Java 直接推理 ONNXONNX Runtime 本身有 Java API理论上 Java 可以自己加载.onnx模型跑推理不需要 Python 介入。但实际落地时会撞上几个硬问题第一YOLO 系列的后处理NMS、坐标还原、多尺度特征图解码在不同版本间差异很大YOLOv5 和 YOLOv8 的输出层格式完全不同用 Java 重写这些逻辑工作量大且容易出错第二Python 侧的onnxruntime和numpy生态成熟预处理letterbox、归一化和后处理都有现成参考实现改起来快第三模型迭代时通常由算法同学用 Python 导出和验证保持 Python 推理链路可以减少沟通成本。所以这个包的选型逻辑是Java 做「工程活」——拉流、解码、组帧、调进程、画框、推流Python 做「算法活」——加载模型、推理、返回原始检测结果。两边通过标准输入输出或临时文件传递数据耦合度低任何一侧出问题都能单独替换。2.2 数据流转的完整链路整个流程可以拆成六步每一步都有明确的输入输出视频流获取Java 侧用javacv或ffmpeg命令行拉取 RTSP/RTMP 流解码成BufferedImage或Mat对象。常见做法是用FFmpegFrameGrabber逐帧读取控制帧率避免堆积。预处理把原始帧 resize 到模型输入尺寸YOLOv5 通常是 640×640做 letterbox padding 保持宽高比像素值归一化到 0~1再转成 Numpy 数组能识别的二进制格式。数据传递Java 把预处理后的数组写入临时文件.npy或裸float32二进制或者通过标准输入管道传给 Python 进程。文件方式更稳管道方式更快但容易死锁。模型调用Python 脚本启动后加载 ONNX 模型读取 Java 传来的数据构造输入 tensor执行session.run()拿到输出。后处理Python 侧做置信度过滤、NMS、坐标映射回原图尺寸把检测框和类别整理成 JSON 返回。结果展示Java 解析 JSON在原帧上绘制矩形框和标签输出到屏幕或推回 RTSP 流。这个链路里最容易被低估的是第 3 步的数据传递。如果每帧都启动一次 Python 进程开销会大到无法接受。实际做法是让 Python 脚本常驻通过循环读取标准输入或监听本地 socketJava 侧维护一个长连接进程。2.3 关键参数与配置项在动手改代码前先搞清楚几个必须对齐的参数参数含义典型值注意点model_input_size模型输入分辨率640×640必须与导出 ONNX 时一致conf_threshold置信度阈值0.25~0.5太低误检多太高漏检iou_thresholdNMS 的 IoU 阈值0.45重叠目标多时适当调高max_detections单帧最大检测数300影响后处理耗时frame_skip跳帧间隔1~3实时性不够时调大input_format输入数据格式float32与 Python 侧np.frombuffer对应这些参数在 Java 侧和 Python 侧要各存一份启动时通过配置文件或命令行参数对齐。我一般会在 Java 的config.properties里定义Python 脚本启动时作为参数传入避免两边硬编码不一致导致「框画歪了」这种玄学问题。3. 动手复现从解压到跑通第一帧检测3.1 环境准备与依赖安装Java 侧需要 JDK 8 以上、Maven 构建核心依赖是javacv-platform和opencvjavacv 自带。Python 侧需要 Python 3.8、onnxruntime、numpy、opencv-python。先确认版本# Java 侧 java -version mvn -version # Python 侧 python --version pip install onnxruntime numpy opencv-python如果用的是 GPU 推理把onnxruntime换成onnxruntime-gpu并确认 CUDA 版本匹配。CPU 推理在 640×640 输入下YOLOv8n 大概能到 30~50ms 一帧够做 15fps 左右的视频分析。3.2 Java 侧拉流与预处理代码Java 侧的核心任务是把视频帧变成 Python 能吃的数组。下面是一个基于FFmpegFrameGrabber的简化实现// VideoFrameGrabber.java import org.bytedeco.javacv.FFmpegFrameGrabber; import org.bytedeco.javacv.Frame; import org.bytedeco.javacv.Java2DFrameConverter; import java.awt.image.BufferedImage; import java.io.*; import java.nio.FloatBuffer; public class VideoFrameGrabber { private static final int INPUT_SIZE 640; public static void main(String[] args) throws Exception { FFmpegFrameGrabber grabber new FFmpegFrameGrabber(rtsp://your-stream-url); grabber.setOption(rtsp_transport, tcp); // 避免 UDP 丢包 grabber.start(); Java2DFrameConverter converter new Java2DFrameConverter(); Process pythonProcess startPythonWorker(); try (OutputStream stdin pythonProcess.getOutputStream(); BufferedReader stdout new BufferedReader( new InputStreamReader(pythonProcess.getInputStream()))) { Frame frame; while ((frame grabber.grabImage()) ! null) { BufferedImage image converter.convert(frame); float[] inputData preprocess(image); // 写入帧长度 帧数据 writeFloatArray(stdin, inputData); stdin.flush(); String result stdout.readLine(); System.out.println(Detection: result); } } grabber.stop(); pythonProcess.destroy(); } private static float[] preprocess(BufferedImage img) { // letterbox resize 归一化输出 CHW 格式 float 数组 // 具体实现略核心是保持宽高比并填充灰边 return new float[3 * INPUT_SIZE * INPUT_SIZE]; } private static void writeFloatArray(OutputStream os, float[] data) throws IOException { ByteBuffer buffer ByteBuffer.allocate(data.length * 4); buffer.asFloatBuffer().put(data); os.write(buffer.array()); } private static Process startPythonWorker() throws IOException { ProcessBuilder pb new ProcessBuilder(python, detector.py, --model, yolov8n.onnx, --conf, 0.4); pb.redirectErrorStream(true); return pb.start(); } }这段代码的关键点rtsp_transport设为tcp能显著减少花屏和丢帧预处理后的数组按float32小端序写入Python 侧用np.frombuffer直接读Python 进程只启动一次循环处理多帧避免反复加载模型。3.3 Python 侧 ONNX 推理脚本Python 脚本负责加载模型、接收数据、推理、返回 JSON。下面是一个支持 YOLOv8 输出的版本# detector.py import sys import json import numpy as np import onnxruntime as ort import argparse def load_model(model_path): session ort.InferenceSession(model_path, providers[CPUExecutionProvider]) input_name session.get_inputs()[0].name return session, input_name def postprocess(outputs, conf_thres0.4, iou_thres0.45): # YOLOv8 输出格式: [1, 84, 8400] predictions np.squeeze(outputs[0]).T # [8400, 84] boxes predictions[:, :4] scores predictions[:, 4:] class_ids np.argmax(scores, axis1) confidences np.max(scores, axis1) mask confidences conf_thres boxes, class_ids, confidences boxes[mask], class_ids[mask], confidences[mask] # NMS 实现略返回 [x1, y1, x2, y2, conf, cls] detections nms(boxes, confidences, class_ids, iou_thres) return detections def main(): parser argparse.ArgumentParser() parser.add_argument(--model, requiredTrue) parser.add_argument(--conf, typefloat, default0.4) args parser.parse_args() session, input_name load_model(args.model) input_shape (1, 3, 640, 640) while True: raw sys.stdin.buffer.read(3 * 640 * 640 * 4) if len(raw) 3 * 640 * 640 * 4: break data np.frombuffer(raw, dtypenp.float32).reshape(input_shape) outputs session.run(None, {input_name: data}) detections postprocess(outputs, args.conf) print(json.dumps(detections.tolist()), flushTrue) if __name__ __main__: main()这里有几个容易翻车的细节sys.stdin.buffer.read必须读满固定字节数否则会截断flushTrue保证 Java 侧能及时读到结果YOLOv5 的输出格式是[1, 25200, 85]后处理要相应调整不能直接套用。3.4 跑通验证与结果查看把 Java 和 Python 两侧代码放好后先单独测 Python 脚本能不能加载模型python detector.py --model yolov8n.onnx --conf 0.4 test_input.bin如果没有报错再启动 Java 主程序。第一次跑建议用本地视频文件代替 RTSP 流排除网络因素。观察控制台输出的检测结果如果框的位置明显偏移检查 letterbox 的 padding 计算和坐标还原逻辑如果置信度全是 0检查输入数据的归一化是否和训练时一致。4. 避坑排查跨语言推理最容易翻车的五个点4.1 检测框整体偏移或缩放错误现象画出来的框位置对不上目标或者框的大小明显不对。原因letterbox 预处理时 padding 的偏移量没有正确传递到后处理阶段。YOLO 的坐标还原需要知道原始图像在 640×640 画布中的实际位置和缩放比例如果 Java 侧算了 padding 但 Python 侧不知道坐标就会错。解决把每帧的scale和pad_x、pad_y一起传给 Python后处理时先减 padding 再除以 scale。或者更简单——Java 侧只做 resize 不做 paddingPython 侧统一做 letterbox保证两边逻辑一致。4.2 Python 进程卡死或结果读取超时现象Java 侧readLine()一直阻塞视频画面不动。原因Python 脚本报错退出但异常信息被redirectErrorStream混进了标准输出Java 读到的不是 JSON 而是错误堆栈或者 Python 侧print没有flush数据留在缓冲区。解决Python 侧所有输出强制flushTrueJava 侧读取时加超时机制超时后销毁进程重启调试阶段把 Python 的 stderr 单独输出到日志文件不要和 stdout 混在一起。4.3 ONNX 模型输出维度与后处理不匹配现象Python 侧reshape报错或者 NMS 后结果为空。原因YOLOv5、YOLOv7、YOLOv8 导出的 ONNX 输出格式不同。YOLOv5 是[1, 25200, 85]YOLOv8 是[1, 84, 8400]前者需要转置后者不需要。如果拿 YOLOv5 的后处理代码跑 YOLOv8 模型维度对不上直接崩。解决在 Python 脚本里根据模型输出 shape 自动判断版本或者启动时通过参数指定--version yolov8。更稳妥的做法是导出 ONNX 时固定输出格式比如用ultralytics导出时加simplifyTrue。4.4 RTSP 流断线后程序不恢复现象网络波动后视频流断了程序一直卡在原地不重连。原因FFmpegFrameGrabber在流断开后grabImage()返回 null但循环没有处理这种情况直接退出了。解决在循环里判断frame null时执行grabber.stop()和grabber.start()重连加重试次数上限和退避间隔。常见做法是重试 3 次每次间隔 2 秒超过后记录日志并告警。4.5 CPU 推理速度跟不上视频帧率现象检测结果滞后于视频画面越跑越慢。原因每帧都做完整推理但 CPU 单帧耗时超过帧间隔导致帧堆积。解决加跳帧逻辑每 2~3 帧处理一次或者用onnxruntime-gpu切到 GPU 推理还可以把输入尺寸从 640 降到 416 或 320速度能提升一倍以上代价是小目标检测精度下降。我一般会先测一下当前硬件的单帧耗时再决定跳帧间隔。5. 进阶技巧让这套方案在生产环境跑得更稳5.1 用共享内存替代标准输入输出标准输入输出在数据量大时会有拷贝开销640×640×3 的 float32 数组每帧约 4.9MB30fps 就是 147MB/s 的管道吞吐容易成为瓶颈。进阶做法是用共享内存Java 侧用FileChannel.map创建内存映射文件Python 侧用np.memmap直接读同一块内存零拷贝。代价是要处理同步问题——Java 写完一帧后通过信号量通知 PythonPython 读完再通知 Java 写下一帧。这套机制在yolo-onnx-java包里没有直接提供但改起来不难适合对延迟敏感的场景。5.2 模型量化与输入尺寸的权衡ONNX 模型支持 INT8 量化能把推理速度提升 2~3 倍模型体积缩小到原来的四分之一。但量化后的精度损失在目标检测任务上比较明显尤其是小目标和密集场景。我的经验是如果检测目标尺寸较大、类别区分度高INT8 量化完全可用如果是安全帽检测、车辆检测这类需要区分相似类别的场景还是用 FP32 或 FP16。输入尺寸方面640 是精度和速度的平衡点416 适合对实时性要求高但目标较大的场景320 基本只能做粗略检测。5.3 多路视频流的进程管理生产环境往往要同时处理多路 RTSP 流。如果每路流都启动一个 Python 进程内存和 CPU 会迅速吃满。更好的做法是让一个 Python 进程服务多路流用多线程或异步 IO 处理推理请求。但 ONNX Runtime 的session.run()本身是线程安全的可以在 Python 侧维护一个线程池Java 侧通过 socket 把不同流的帧发到同一个 Python 服务带上流 ID 做区分。这样模型只加载一次显存和内存占用都可控。5.4 验证检测结果是否正确的土办法调参调到最后怎么确认检测结果是对的我一般会做两件事第一用一张已知答案的测试图比如bus.jpg跑一遍把画框后的图保存下来肉眼比对确认框的位置和类别都对第二统计连续 100 帧的检测数量如果波动特别大比如从 5 个跳到 50 个说明置信度阈值或 NMS 参数有问题。还有一个笨办法但很有效把 Python 侧返回的原始输出和 Java 侧画框后的结果同时打印出来逐帧对比坐标偏移超过 2 个像素就说明预处理或后处理有 bug。从那以后我每次接入新的 ONNX 模型都会先用一张固定测试图跑通全链路确认坐标还原无误后再接视频流。这个习惯帮我省掉了至少三次「框画歪了但找不到原因」的返工。希望帮到你。本文还有配套的精品资源点击获取