简介:本资源面向具备一定Java与深度学习基础的开发者,提供在Java应用中调用Python端YOLO ONNX模型实现AI视频目标检测与识别的完整源代码方案,支持YOLOv5、YOLOv7、YOLOv8三种主流模型。资源包共52个文件,包含12个Java源码文件、4个ONNX模型文件、23张PNG截图与3个GIF演示动图,另有pom.xml构建配置、DLL依赖库及README说明文档,压缩包约173MB。方案覆盖图像缩放、裁剪、灰度化等预处理,以及模型输出解析、筛选、可视化等后处理环节,并集成RTSP与RTMP实时流协议,可直接从实时视频流中提取帧数据送入模型完成识别与跟踪。目前已有1502人学习下载,适合需要将YOLO目标检测能力嵌入Java后端或视频监控系统的开发者参考,借助现成工程结构与示例素材快速跑通从模型加载到视频流识别的完整链路。
1. Java 调用 Python YOLO ONNX 模型做视频识别:这套源码到底能不能直接跑
上周有个做安防平台的兄弟找我,说他手上有个 Java 写的视频管理后台,老板突然要求加 AI 识别功能,指定要 YOLO,还必须是本地推理不能调云端 API。他第一反应是找 Java 版的 YOLO 实现,折腾了两天发现要么版本老旧只支持 YOLOv5,要么依赖的 NDK 版本跟现有环境打架。后来他换了个思路——Java 负责业务调度和视频流管理,Python 负责模型推理,两边通过进程间通信对接。这套「Java 调用 Python YOLO ONNX 模型」的源码包,走的就是这条路子,支持 yolov5、yolov7、yolov8 三个主流版本的 ONNX 模型加载和视频识别。它解决的核心问题不是「怎么在 Java 里重写一个推理引擎」,而是「怎么让已有的 Java 系统低成本接入 Python 生态里的 AI 能力」。适合谁用?手上有 Java 后端、需要快速集成目标检测功能、又不想把整个技术栈推倒重来的团队。下面我从这套源码的实际结构拆起,把部署、调用、参数、坑点一次讲透。
2. 源码结构拆解:Java 端和 Python 端各自管什么
2.1 Java 侧的职责边界与进程通信方式
这套源码的 Java 部分不碰模型推理,它只做三件事:管理视频源(RTSP 流、本地文件、摄像头设备)、按帧率抽帧后把图像数据传给 Python 进程、接收 Python 返回的检测结果并做业务层处理(比如画框、存库、触发告警)。Java 调用 Python 的方式常见有两种:一种是ProcessBuilder启动 Python 脚本,通过标准输入输出传数据;另一种是起一个 Python HTTP 服务,Java 用 HTTP 客户端调。这套源码用的是第一种,理由是延迟更低、不占端口、部署时少一层网络配置。
Java 侧的核心类一般长这样:
// Java 端启动 Python 推理进程并发送图像路径 public class YoloInvoker { private Process pythonProcess; private BufferedWriter writer; private BufferedReader reader; public void startPythonService(String pythonPath, String scriptPath) throws IOException { ProcessBuilder pb = new ProcessBuilder(pythonPath, scriptPath); pb.redirectErrorStream(true); // 把 stderr 合并到 stdout,方便排查 Python 报错 pythonProcess = pb.start(); writer = new BufferedWriter(new OutputStreamWriter(pythonProcess.getOutputStream())); reader = new BufferedReader(new InputStreamReader(pythonProcess.getInputStream())); } public String detect(String imagePath) throws IOException { writer.write(imagePath + "\n"); // 一行一个路径,Python 侧按行读 writer.flush(); return reader.readLine(); // 返回 JSON 格式的检测结果 } }这里的关键参数是redirectErrorStream(true),不加这行的话 Python 端的异常信息会跑到 stderr,Java 读不到,排查问题时只能看到进程卡死。另一个注意点是writer.flush()必须调,否则数据留在缓冲区里,Python 侧一直等输入,两边互相干等——这个坑我后面还会细说。
2.2 Python 侧的 ONNX Runtime 加载与推理流程
Python 端拿到图像路径后,用 OpenCV 读图、预处理、送进 ONNX Runtime 推理、做 NMS 后处理、把结果序列化成 JSON 返回。这套源码支持 yolov5、yolov7、yolov8 三个版本,它们的预处理和后处理逻辑有差异,源码里用了一个配置字典来区分:
# Python 端根据模型版本选择预处理参数 MODEL_CONFIG = { "yolov5": {"input_size": (640, 640), "conf_thres": 0.25, "iou_thres": 0.45, "num_classes": 80}, "yolov7": {"input_size": (640, 640), "conf_thres": 0.25, "iou_thres": 0.45, "num_classes": 80}, "yolov8": {"input_size": (640, 640), "conf_thres": 0.25, "iou_thres": 0.45, "num_classes": 80}, } def preprocess(image, version): size = MODEL_CONFIG[version]["input_size"] img = cv2.resize(image, size) img = img[:, :, ::-1].transpose(2, 0, 1) # BGR 转 RGB,HWC 转 CHW img = np.ascontiguousarray(img).astype(np.float32) / 255.0 return np.expand_dims(img, axis=0)input_size三个版本默认都是 640×640,但 yolov5 和 yolov7 的输出层结构不同,yolov8 更是换成了 anchor-free 的解码方式。源码里对 yolov8 单独写了一个postprocess_v8函数,因为它的输出格式是[batch, 84, 8400],前 4 个通道是框坐标,后面 80 个是类别分数,跟 v5/v7 的[batch, 25200, 85]完全不一样。如果你拿 v5 的后处理去解 v8 的输出,结果就是一堆乱框或者直接报维度错误。
2.3 模型文件与依赖版本对照
源码包里通常带一个requirements.txt和模型转换脚本。ONNX 模型不是直接拿.pt文件就能用的,需要先用torch.onnx.export转一道。三个版本的导出命令有细微差别:
| 版本 | 导出方式 | 输出节点名 | 动态维度支持 |
|---|---|---|---|
| yolov5 | python export.py --weights yolov5s.pt --include onnx | output | 需加--dynamic |
| yolov7 | python export.py --weights yolov7.pt --grid --end2end | output | 默认支持 |
| yolov8 | yolo export model=yolov8n.pt format=onnx | output0 | 默认支持 |
依赖方面,ONNX Runtime 建议用onnxruntime-gpu如果机器有 NVIDIA 显卡,否则用onnxruntimeCPU 版。OpenCV 用opencv-python就够,不需要 contrib 版本。numpy 版本别超过 1.24,否则某些老版 onnxruntime 会报np.float被移除的错误——这是血泪经验,我见过至少三个项目卡在这个版本兼容上。
3. 从零跑通:环境配置与最小可复现示例
3.1 环境准备与依赖安装
先确认 Java 和 Python 的版本。Java 建议 JDK 11 或 17,Python 建议 3.8 到 3.10 之间。Python 3.11 也能跑,但部分 onnxruntime 版本还没跟上,容易出玄学问题。安装依赖的命令:
# 创建虚拟环境,避免污染系统 Python python -m venv yolo_env source yolo_env/bin/activate # Windows 用 yolo_env\Scripts\activate # 安装核心依赖 pip install onnxruntime-gpu==1.16.0 # 有 GPU 用这个 # pip install onnxruntime==1.16.0 # 纯 CPU 用这个 pip install opencv-python==4.8.1.78 pip install numpy==1.24.3onnxruntime-gpu的版本要和 CUDA 版本对上。1.16.0 对应 CUDA 11.8,如果你机器上是 CUDA 12.x,得换 onnxruntime 1.17 以上。装完之后用一行代码验证:
import onnxruntime as ort print(ort.get_available_providers()) # 有 GPU 的话应该输出 ['CUDAExecutionProvider', 'CPUExecutionProvider']如果只输出['CPUExecutionProvider'],说明 GPU 版没装对,要么是 CUDA 路径没配好,要么是版本不匹配。CPU 推理也能跑,但视频识别场景下帧率会从 30fps 掉到 3 到 5fps,这个差距在实际项目里是致命的。
3.2 Python 推理脚本的完整实现
下面是一个精简版的推理脚本,保留了核心逻辑,去掉了业务层的花哨封装:
import sys import json import cv2 import numpy as np import onnxruntime as ort class YoloOnnxDetector: def __init__(self, model_path, version="yolov8"): self.session = ort.InferenceSession(model_path, providers=['CUDAExecutionProvider', 'CPUExecutionProvider']) self.input_name = self.session.get_inputs()[0].name self.version = version self.conf_thres = 0.25 self.iou_thres = 0.45 def detect(self, image_path): img = cv2.imread(image_path) if img is None: return {"error": "image not found"} h, w = img.shape[:2] blob = self.preprocess(img) outputs = self.session.run(None, {self.input_name: blob}) boxes = self.postprocess(outputs, w, h) return {"boxes": boxes, "count": len(boxes)} def preprocess(self, img): resized = cv2.resize(img, (640, 640)) rgb = cv2.cvtColor(resized, cv2.COLOR_BGR2RGB) blob = rgb.transpose(2, 0, 1).astype(np.float32) / 255.0 return np.expand_dims(blob, axis=0) def postprocess(self, outputs, orig_w, orig_h): # yolov8 输出格式: [1, 84, 8400] pred = outputs[0][0].T # 转成 [8400, 84] scores = pred[:, 4:] class_ids = np.argmax(scores, axis=1) confidences = np.max(scores, axis=1) mask = confidences > self.conf_thres boxes = pred[mask, :4] # 这里省略 NMS 细节,源码里有完整实现 return boxes.tolist() if __name__ == "__main__": detector = YoloOnnxDetector("yolov8n.onnx", version="yolov8") for line in sys.stdin: path = line.strip() if not path: continue result = detector.detect(path) print(json.dumps(result), flush=True) # flush 必须加,否则 Java 端读不到这个脚本的核心在于sys.stdin逐行读路径、print(json.dumps(...), flush=True)逐行输出结果。flush=True是必须的,Python 默认会缓冲输出,不加的话 Java 端会一直阻塞在readLine()上。这个坑我在两个项目里都踩过,表现是「Java 端卡死,Python 端看起来正常」,实际上 Python 已经把结果算完了,只是没吐出来。
3.3 Java 端调用与结果解析
Java 端拿到 JSON 字符串后,用 Jackson 或 Gson 解析就行。关键是要处理 Python 进程的生命周期——别每次检测都重启进程,那样光加载模型就要好几秒。正确的做法是启动时初始化一次,后续复用:
// 初始化时启动一次,后续复用 public class YoloService { private YoloInvoker invoker; public void init() throws IOException { invoker = new YoloInvoker(); invoker.startPythonService("python", "detector.py"); } public DetectionResult detect(String imagePath) throws IOException { String json = invoker.detect(imagePath); ObjectMapper mapper = new ObjectMapper(); return mapper.readValue(json, DetectionResult.class); } public void shutdown() { invoker.close(); // 记得关进程,否则会残留僵尸进程 } }shutdown()方法别漏了。Java 的Process对象如果不显式销毁,Python 进程会一直挂在后台。在 Web 应用里,这个通常放在ServletContextListener的contextDestroyed里调。我见过一个项目因为没关进程,跑了一周之后服务器上堆了上百个 Python 僵尸进程,内存直接被吃满。
4. 避坑排查:Java 调 Python 做视频识别的五个翻车现场
4.1 进程阻塞读不到输出
现象:Java 端调用readLine()一直不返回,Python 端日志显示推理已完成。
原因:Python 的print默认带缓冲,输出没刷到 stdout。或者 Java 端没读 stderr,Python 报错信息被堵在管道里导致缓冲区满。
解决:Python 端所有输出加flush=True;Java 端ProcessBuilder加redirectErrorStream(true),并且用独立线程读 stderr,避免管道缓冲区写满后子进程阻塞。
4.2 ONNX 模型加载报维度不匹配
现象:InvalidArgument: Got invalid dimensions for input: images。
原因:导出 ONNX 时没指定动态 batch 维度,模型固定了 batch=1,但推理时传了 batch=2 或者输入尺寸跟模型期望的不一致。
解决:导出时加--dynamic参数(yolov5)或确认导出配置里dynamic_axes包含 batch 维度。推理时严格按模型元数据里的尺寸做预处理,别自己拍脑袋改。
4.3 GPU 推理结果和 CPU 不一致
现象:同一张图,CPU 推理能检出 5 个目标,GPU 推理只检出 3 个,或者框的位置有细微偏移。
原因:浮点精度差异。GPU 用 FP16 推理时精度损失比 CPU 的 FP32 大,某些置信度在阈值边缘的目标会被过滤掉。
解决:如果业务对精度敏感,强制用 FP32 推理;或者在 GPU 推理时把conf_thres稍微调低 0.05,用 NMS 的iou_thres来兜底。源码里默认用的是 FP32,就是为了避免这个玄学问题。
4.4 视频抽帧后图像路径传参失败
现象:Java 端传了带空格的路径,Python 端cv2.imread返回None。
原因:路径里有空格或中文,ProcessBuilder传参时被截断,或者 Python 端读到的字符串带了不可见字符。
解决:Java 端传路径前做 URL 编码或者用 Base64 编码,Python 端解码后再读。更简单的做法是统一用临时文件目录,路径里只留英文和数字。源码里用的是 Base64 编码方案,多一层编解码但省心。
4.5 多线程调用时结果错乱
现象:Java 端开了多个线程同时调detect(),返回的 JSON 对不上请求的图像。
原因:BufferedWriter和BufferedReader不是线程安全的,多个线程同时写读同一个管道,数据串了。
解决:给detect()方法加synchronized,或者用线程池限制并发数为 1。如果确实需要高并发,起多个 Python 进程做进程池,每个进程独立管道。源码里默认是单进程加锁的方案,适合中小规模场景。
5. 进阶技巧:用批处理和异步队列把吞吐量拉上去
单进程逐帧推理的瓶颈很明显——GPU 利用率上不去,因为每次只送一张图。我一般会做两件事:Python 侧支持 batch 推理,Java 侧用异步队列攒批。
Python 侧改动不大,把preprocess改成接收图像列表,拼成[N, 3, 640, 640]的 blob 一次送进 session:
def detect_batch(self, image_paths): blobs = [] for path in image_paths: img = cv2.imread(path) if img is None: continue blobs.append(self.preprocess(img)) if not blobs: return [] batch = np.concatenate(blobs, axis=0) # 拼成 [N, 3, 640, 640] outputs = self.session.run(None, {self.input_name: batch}) return self.postprocess_batch(outputs)Java 侧用一个BlockingQueue攒够 8 张或者等 50ms 就触发一次批量调用。实测在 2080Ti 上,单张推理 15ms,batch=8 时单张摊到 6ms,吞吐量翻了一倍多。但 batch 别开太大,超过 16 之后显存占用上去了,延迟反而增加,而且一旦某张图预处理失败,整批都得重来。
另一个技巧是模型预热。ONNX Runtime 第一次推理会做图优化和内存分配,耗时可能是后续的 10 倍。启动后先拿一张空白图跑一次session.run,把预热做掉,后续请求就不会有首帧卡顿。这个在视频流场景里特别重要,否则第一帧的延迟会让上层以为服务挂了。
验证方法很简单:用同一段视频分别跑单张和批量模式,对比总耗时和 GPU 利用率(nvidia-smi -l 1看)。如果 GPU 利用率从 20% 涨到 60% 以上,说明批处理生效了。从那以后我每次部署这类 Java 调 Python 的推理服务,都会在启动脚本里强制加一行预热调用,并且把 batch size 和超时时间做成配置项,方便不同机器上调优。希望帮到你。
本文还有配套的精品资源,点击获取