简介:本资源面向需要在 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 直接推理 ONNX
ONNX 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 脚本常驻,通过循环读取标准输入或监听本地 socket,Java 侧维护一个长连接进程。
2.3 关键参数与配置项
在动手改代码前,先搞清楚几个必须对齐的参数:
| 参数 | 含义 | 典型值 | 注意点 |
|---|---|---|---|
model_input_size | 模型输入分辨率 | 640×640 | 必须与导出 ONNX 时一致 |
conf_threshold | 置信度阈值 | 0.25~0.5 | 太低误检多,太高漏检 |
iou_threshold | NMS 的 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和opencv(javacv 自带)。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_thres=0.4, iou_thres=0.45): # YOLOv8 输出格式: [1, 84, 8400] predictions = np.squeeze(outputs[0]).T # [8400, 84] boxes = predictions[:, :4] scores = predictions[:, 4:] class_ids = np.argmax(scores, axis=1) confidences = np.max(scores, axis=1) 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', required=True) parser.add_argument('--conf', type=float, default=0.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, dtype=np.float32).reshape(input_shape) outputs = session.run(None, {input_name: data}) detections = postprocess(outputs, args.conf) print(json.dumps(detections.tolist()), flush=True) if __name__ == '__main__': main()这里有几个容易翻车的细节:sys.stdin.buffer.read必须读满固定字节数,否则会截断;flush=True保证 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 不做 padding,Python 侧统一做 letterbox,保证两边逻辑一致。
4.2 Python 进程卡死或结果读取超时
现象:Java 侧readLine()一直阻塞,视频画面不动。
原因:Python 脚本报错退出但异常信息被redirectErrorStream混进了标准输出,Java 读到的不是 JSON 而是错误堆栈;或者 Python 侧print没有flush,数据留在缓冲区。
解决:Python 侧所有输出强制flush=True;Java 侧读取时加超时机制,超时后销毁进程重启;调试阶段把 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导出时加simplify=True。
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.9MB,30fps 就是 147MB/s 的管道吞吐,容易成为瓶颈。进阶做法是用共享内存:Java 侧用FileChannel.map创建内存映射文件,Python 侧用np.memmap直接读同一块内存,零拷贝。代价是要处理同步问题——Java 写完一帧后通过信号量通知 Python,Python 读完再通知 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 模型,都会先用一张固定测试图跑通全链路,确认坐标还原无误后再接视频流。这个习惯帮我省掉了至少三次「框画歪了但找不到原因」的返工。希望帮到你。
本文还有配套的精品资源,点击获取