news 2026/10/13 4:04:15

Java调用Python YOLO ONNX模型做视频识别:从单路到多路实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java调用Python YOLO ONNX模型做视频识别:从单路到多路实战

简介:这是一套面向Java开发者与AI应用工程师的跨语言目标检测实战源码,解决Java项目难以直接调用YOLO系列模型完成视频识别的问题。资源以Java调用Python端YOLO ONNX模型为主线,兼容YOLOv5、YOLOv7、YOLOv8三种主流网络,并集成RTSP、RTMP实时流处理能力,可直接从视频流中抽帧送入模型完成目标检测与跟踪。压缩包共52个文件,约173MB,包含12个Java源码文件、4个onnx模型文件、23张png与3张jpg效果截图、3个gif演示动图,以及pom.xml、README.md、dll依赖和license等配套内容,覆盖工程配置、模型加载、预处理与后处理全流程。代码中对图像缩放、裁剪、灰度化等预处理,以及输出解析、筛选、可视化等后处理均有体现,便于读者理解Java与Python协同推理的完整链路。目前已有1502人学习下载,适合需要将YOLO检测能力嵌入Java服务或视频监控系统的开发者参考复用。

1. Java 调用 Python YOLO ONNX 模型做视频识别:为什么这条链路值得搭

很多做后端或桌面端的团队,视频识别需求来得都很突然:产品说要在现有 Java 系统里加一路摄像头分析,最好还能识别出人、车、安全帽。你手里现成的资产往往是一份 Python 训练出来的 YOLO 权重,可能是 yolov5、yolov7 或 yolov8,导出成 ONNX 之后,问题就变成了——Java 怎么把它跑起来,还要跑在视频流上。

直接上 Java 深度学习框架不是不行,但模型转换、算子兼容、前后处理对齐,每一步都是玄学。更稳的做法是让 Python 负责推理,Java 负责业务调度和视频解码,两边用进程调用或本地服务通信。这条链路的好处是:模型侧完全复用 Python 生态,yolov5/yolov7/yolov8 的 ONNX 都能吃;Java 侧不用碰张量计算,只做编排。适合已有 Java 主系统、又不想重写推理层的团队,也适合想快速验证视频识别可行性的个人开发者。

2. 三种 Java 调 Python 的落地方式:进程、HTTP、gRPC 怎么选

2.1 先想清楚推理进程的生命周期

Java 调 Python 最容易被忽略的是进程模型。每次识别都Runtime.exec起一个 Python,模型加载就要几百毫秒到几秒,视频按帧跑根本扛不住。所以核心问题是:Python 推理进程是常驻还是短命。

常见做法有三类。第一类是短进程,Java 把图片路径或 base64 传给 Python 脚本,脚本跑完输出 JSON 就退出,适合低频、离线批处理。第二类是常驻 HTTP 服务,Python 用 FastAPI 或 Flask 起一个本地端口,Java 用 HttpClient 发请求,模型只加载一次,适合视频这种高频场景。第三类是 gRPC 或本地 socket,延迟更低,但要多写协议定义。

我一般会先上 HTTP 常驻服务,理由是调试直观、跨语言无痛、出问题能单独用 curl 验证。等吞吐真的顶不住,再考虑换 gRPC 或共享内存传帧。

2.2 用 FastAPI 起一个常驻推理服务

先看 Python 侧。下面这段代码同时兼容 yolov5、yolov7、yolov8 导出的 ONNX,关键在于统一输入输出格式,而不是依赖某个版本的官方推理脚本。

# infer_server.py import io import numpy as np import onnxruntime as ort from fastapi import FastAPI, UploadFile from PIL import Image app = FastAPI() # 只加载一次,常驻内存 session = ort.InferenceSession( "yolov8n.onnx", providers=["CPUExecutionProvider"] # 有 GPU 可换 CUDAExecutionProvider ) input_name = session.get_inputs()[0].name input_shape = session.get_inputs()[0].shape # [1,3,640,640] def letterbox(img, new_shape=640): # 保持比例缩放并填充,避免形变影响精度 w, h = img.size scale = min(new_shape / w, new_shape / h) nw, nh = int(w * scale), int(h * scale) img_resized = img.resize((nw, nh)) canvas = Image.new("RGB", (new_shape, new_shape), (114, 114, 114)) canvas.paste(img_resized, ((new_shape - nw) // 2, (new_shape - nh) // 2)) return canvas, scale, (new_shape - nw) // 2, (new_shape - nh) // 2 def preprocess(img): img, scale, dx, dy = letterbox(img) arr = np.asarray(img, dtype=np.float32) / 255.0 arr = arr.transpose(2, 0, 1)[None, ...] # HWC -> NCHW return np.ascontiguousarray(arr), scale, dx, dy @app.post("/detect") async def detect(file: UploadFile): raw = await file.read() img = Image.open(io.BytesIO(raw)).convert("RGB") tensor, scale, dx, dy = preprocess(img) outputs = session.run(None, {input_name: tensor}) # 后处理省略,返回原始输出维度便于调试 return {"shape": [list(o.shape) for o in outputs]}

这段代码里几个点必须说清楚。ort.InferenceSession放在模块顶层,保证服务启动时只加载一次模型,这是常驻服务的意义所在。providers决定用 CPU 还是 GPU,装了 onnxruntime-gpu 才能写 CUDAExecutionProvider,否则会直接报错。letterbox是 YOLO 系列通用的预处理,yolov5、yolov7、yolov8 都吃这一套,区别只在后处理的解码方式。输入张量必须是 NCHW 且连续内存,np.ascontiguousarray这行别省,否则 onnxruntime 可能报维度或内存错误。

2.3 Java 侧用 HttpClient 发帧并解析结果

Java 这边不引入任何深度学习依赖,只用 JDK 自带的 HttpClient 和 Jackson。

// YoloClient.java import java.net.URI; import java.net.http.*; import java.nio.file.*; import java.time.Duration; public class YoloClient { private static final HttpClient CLIENT = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .build(); public static String detect(Path imagePath) throws Exception { // 手工拼 multipart,避免引入额外依赖 String boundary = "----YoloBoundary" + System.currentTimeMillis(); byte[] fileBytes = Files.readAllBytes(imagePath); var body = new java.io.ByteArrayOutputStream(); body.write(("--" + boundary + "\r\n").getBytes()); body.write(("Content-Disposition: form-data; name=\"file\"; filename=\"frame.jpg\"\r\n").getBytes()); body.write("Content-Type: image/jpeg\r\n\r\n".getBytes()); body.write(fileBytes); body.write(("\r\n--" + boundary + "--\r\n").getBytes()); HttpRequest req = HttpRequest.newBuilder() .uri(URI.create("http://127.0.0.1:8000/detect")) .timeout(Duration.ofSeconds(10)) .header("Content-Type", "multipart/form-data; boundary=" + boundary) .POST(HttpRequest.BodyPublishers.ofByteArray(body.toByteArray())) .build(); HttpResponse<String> resp = CLIENT.send(req, HttpResponse.BodyHandlers.ofString()); return resp.body(); } }

HttpClient是 JDK 11 之后自带的,不用引第三方库。multipart 手工拼是为了减少依赖,如果项目里已经有 OkHttp 或 Apache HttpClient,直接用它们的 multipart 更省事。超时时间要设,视频帧偶尔卡一下不能让 Java 线程无限等。返回的 JSON 里包含检测框和类别,Java 侧再映射成业务对象。

2.4 视频流怎么拆帧、怎么控节奏

视频识别不是单张图片,Java 侧要负责拆帧。常见做法是用 JavaCV 或 ffmpeg 命令行抽帧,再逐帧发给 Python。

// 用 ffmpeg 按固定帧率抽帧,避免逐帧处理拖垮服务 // ffmpeg -i input.mp4 -vf fps=5 frames/%06d.jpg

抽帧率是关键参数。原始视频 25fps,全抽就是每秒 25 次推理,CPU 上大概率跑不动。我一般先降到 5fps,看业务能不能接受,再往上调。抽出来的帧按序号命名,Java 顺序读取、发送、收集结果,最后按时间戳回填到视频时间轴。如果要做实时流,就用 ffmpeg 推流到本地管道,Java 读管道里的帧,省去落盘。

3. YOLOv5/v7/v8 的 ONNX 输出差异与后处理对齐

3.1 三个版本的输出张量到底差在哪

这是整条链路最容易翻车的地方。yolov5 和 yolov7 的 ONNX 输出通常是[1, 25200, 85]这种形状,85 = 4 个框坐标 + 1 个置信度 + 80 类。yolov8 变了,输出是[1, 84, 8400],84 = 4 个框 + 80 类,而且没有独立的置信度维度,类别分数直接乘在框上。如果你拿 yolov5 的后处理去解 yolov8 的输出,结果要么全空要么全是乱框。

版本典型输出形状是否含 objectness解码方式
yolov5[1, 25200, 85]有先筛 objectness 再取类别
yolov7[1, 25200, 85]有同 yolov5,部分导出带多输出
yolov8[1, 84, 8400]无直接取类别最大分,转置后 NMS

导出时的参数也会影响形状。yolov5 导出加--include onnx,默认就是上面这种。yolov8 用yolo export model=yolov8n.pt format=onnx,默认输出[1,84,8400]。如果加了nms=True,输出会变成已经做过 NMS 的结果,形状又不一样,Java 侧解析逻辑要跟着改。

3.2 写一个版本无关的后处理函数

与其为每个版本写一套,不如在 Python 侧统一成一种输出格式再返回给 Java。

def postprocess(outputs, conf_thres=0.25, iou_thres=0.45): pred = outputs[0] # 统一转成 [N, 4+nc] 且带 objectness 的中间格式 if pred.ndim == 3 and pred.shape[1] < pred.shape[2]: # yolov8: [1,84,8400] -> [8400,84] pred = pred[0].transpose(0, 1) boxes, scores = pred[:, :4], pred[:, 4:] conf = scores.max(axis=1) cls = scores.argmax(axis=1) else: # yolov5/v7: [1,25200,85] pred = pred[0] boxes, obj, scores = pred[:, :4], pred[:, 4], pred[:, 5:] conf = obj * scores.max(axis=1) cls = scores.argmax(axis=1) mask = conf > conf_thres boxes, conf, cls = boxes[mask], conf[mask], cls[mask] # 这里接 NMS,xywh -> xyxy 后做 IoU 抑制 return boxes, conf, cls

判断分支靠的是维度关系:yolov8 的通道数小于候选框数,转置后每行是一个候选。yolov5/v7 反过来。conf_thres控制召回,视频场景我一般设 0.25 到 0.4,太低会满屏误检,太高会漏掉远处小目标。iou_thres控制重叠框合并,0.45 是通用值,密集场景可以降到 0.3。

3.3 坐标还原:别让框画偏了

预处理做了 letterbox,后处理必须把坐标还原回原图。这一步错了,框的位置会整体偏移,而且偏移量随图片宽高比变化,很难一眼看出。

def scale_boxes(boxes, scale, dx, dy, orig_w, orig_h): # boxes 是 xywh 格式,先转 xyxy x1 = (boxes[:, 0] - boxes[:, 2] / 2 - dx) / scale y1 = (boxes[:, 1] - boxes[:, 3] / 2 - dy) / scale x2 = (boxes[:, 0] + boxes[:, 2] / 2 - dx) / scale y2 = (boxes[:, 1] + boxes[:, 3] / 2 - dy) / scale return np.stack([x1, y1, x2, y2], axis=1).clip(0, [orig_w, orig_h, orig_w, orig_h])

scale、dx、dy就是预处理时算出来的缩放比和填充偏移,必须一路传到后处理。clip 是防止坐标越界,画框时超出画面会报错。这套还原逻辑对三个版本都适用,因为 letterbox 是共通的。

4. 视频识别链路的避坑与排查:五个真实踩过的坑

4.1 坑一:模型加载慢,每帧都重新加载

现象是单张图片识别要两三秒,视频完全跑不动。原因是 Java 每次调用都起新 Python 进程,模型反复加载。解决就是改成常驻服务,模型在服务启动时加载一次。如果必须用短进程,至少把模型路径缓存到本地 SSD,别放网络盘。

4.2 坑二:ONNX 输出形状和预期不符

现象是后处理报维度错误,或者框全是 NaN。原因是导出参数不同,比如 yolov8 加了nms=True,或者 yolov5 导出时改了--img-size。解决是先用一段最小脚本打印session.get_outputs()[0].shape,确认形状再写后处理,别凭记忆。

4.3 坑三:Java 传图 base64 导致内存暴涨

现象是跑一段时间后 Java 堆内存持续上涨。原因是把每帧都转成 base64 字符串,字符串对象在堆里堆积。解决是改用 multipart 二进制传输,或者用本地文件路径传递,Java 只传路径,Python 自己读文件。视频场景帧率高,二进制传输是必须的。

4.4 坑四:CPU 推理线程数没设,多路视频互相抢核

现象是单路视频还行,加到三路就全部变慢。原因是 onnxruntime 默认用满所有 CPU 核,多进程互相抢占。解决是在SessionOptions里限制intra_op_num_threads,每路服务给 2 到 4 个线程,按核数除以路数来分。

opts = ort.SessionOptions() opts.intra_op_num_threads = 4 session = ort.InferenceSession("yolov8n.onnx", opts, providers=["CPUExecutionProvider"])

4.5 坑五:抽帧率设太高,识别结果时间轴对不上

现象是识别结果和视频画面对不上,或者结果数量远少于预期。原因是抽帧率和回填时间戳没对齐。解决是抽帧时记录每帧的原始时间戳,识别结果按时间戳回填,而不是按帧序号。ffmpeg 抽帧可以用-frame_pts 1保留时间信息,或者 Java 侧自己按 fps 算。

5. 把单路 Demo 推到多路可用:几个我常用的调优习惯

单路跑通只是起点,真正上线要考虑多路并发和稳定性。我一般会先做一个压测脚本,用同一段视频循环发送,观察 Python 服务的 CPU、内存和延迟曲线。如果延迟随并发数线性上升,说明推理是瓶颈,要么加 GPU,要么降抽帧率,要么上模型量化。

模型量化是最直接的提速手段。ONNX 支持动态量化,把权重从 FP32 降到 INT8,CPU 上通常能快 1.5 到 2 倍,精度掉一点点。

from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic("yolov8n.onnx", "yolov8n_int8.onnx", weight_type=QuantType.QUInt8)

量化后的模型直接用同样的InferenceSession加载,Java 侧完全无感。但要注意,量化对 yolov8 的检测头部分有时会掉点明显,建议量化后在验证集上跑一遍 mAP,掉超过 3 个点就换回 FP32。

另一个习惯是给 Python 服务加健康检查接口。Java 侧在发送帧之前先探活,服务挂了就重启或切备用实例,别让视频流卡死。健康检查就返回模型名和当前队列长度,简单但能救命。

@app.get("/health") def health(): return {"model": "yolov8n", "status": "ok"}

最后说个我自己的教训。早期做这类链路时,我总想把所有逻辑塞进 Java,觉得跨语言调用不优雅。结果每次模型换版本,Java 侧后处理都要重写一遍,改到怀疑人生。后来把后处理全部收进 Python,Java 只做传输和业务映射,换模型只动 Python 一个文件,省下的时间够喝好几杯咖啡。跨语言链路的分工原则就一条:谁离模型近,谁负责模型相关的一切。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 4:03:51

Linux Socket编程预备知识:从网络分层到字节序实战

套接字Socket编程这块,很多人一上来就打开编辑器写代码,结果被各种编译错误、段错误、连接失败搞得一头雾水。其实问题多半不是出在代码上,而是预备知识没补齐。这篇东西就是写给那些正准备接触Linux网络编程的人,先从预备阶段把网络分层、地址结构、字节序、端口这些底层逻辑理…

作者头像 李华
网站建设 2026/10/13 4:03:51

代码随想录刷题大纲:算法面试高效备战路线

这套刷题大纲的价值&#xff0c;不在于它列了多少道题&#xff0c;而在于它把“算法面试准备”这件事从一团乱麻整理成了一条可以照着走的路线。很多准备技术面试的朋友都听说过“代码随想录”这四个字&#xff0c;甚至在各种经验帖里看到过别人推荐&#xff0c;但真正打开之后…

作者头像 李华
网站建设 2026/10/13 4:03:45

Ext2文件系统深度解析:块组、inode与掉电恢复实践

老规矩&#xff0c;先说个场景&#xff1a;你在某块嵌入式板子上格式化SD卡时&#xff0c;或者翻一本老Linux资料时&#xff0c;总会撞见Ext2这个名字。很多人是从Ext4开始认识Linux文件系统的&#xff0c;却不知道Ext2才是那个让Linux在文件系统层面站稳脚跟的元老。这篇不是把…

作者头像 李华
网站建设 2026/10/13 4:03:30

Unity CommandBuffer从入门到实战:渲染管线定制核心指南

1. 项目概述&#xff1a;为什么一个Unity新手要死磕CommandBuffer&#xff1f;“Unity菜鸟开发纪要——CommandBuffer”这个标题乍看像是一篇带点自嘲的笔记&#xff0c;但背后藏着的是很多刚从UGUI简单脚本过渡到真实渲染管线开发的开发者&#xff0c;第一次撞上渲染底层时的真…

作者头像 李华
网站建设 2026/10/13 4:02:54

并查集动态维护连通块大小:‘星球联盟‘题解与易错点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 4:02:45

SpringBoot二手滑板交易系统实战:从数据库设计到订单状态机

最近在做一个面向滑板圈子的二手交易项目&#xff0c;整体定位是基于Java和SpringBoot的二手滑板交易系统&#xff0c;同时也承载着滑板商城销售的功能。这个项目从需求调研到数据库设计&#xff0c;再到核心交易流程落地&#xff0c;踩了不少坑&#xff0c;也整理出一些对新手…

作者头像 李华