资讯详情

资讯详情

Java服务集成YOLO视频检测:Python调ONNX的工程实践

简介面向 Java 开发者的一套跨语言目标检测集成方案演示如何通过 Java 调用 Python 加载 YOLO ONNX 模型对视频流进行目标检测与识别支持 YOLOv5、YOLOv7、YOLOv8适合需要将深度学习模型嵌入 Java 后端或视频监控系统的工程师。压缩包共 68 个文件约 272MB包含 17 个 Java 源码、1 个 Python 脚本、5 个 ONNX 模型文件以及 28 个 PNG、5 个 JPG、4 个 GIF、2 个 MP4 等图示、录屏与演示视频另有 XML 配置、DLL 动态库等辅助文件。内容覆盖视频流获取、预处理、数据转换为 Numpy 数组、JNI 调用 Python 脚本、模型推理、后处理及识别框绘制等关键环节demo 目录、多个 ONNX 模型和 mp4/gif 示例可用于直观比对检测效果预处理、推理、后处理函数按模块拆分便于直接移植或扩展为 RTSP/RTMP 视频流接入。目前已有 265 人学习下载可作为跨语言模型集成场景的实战参照。1. Java 服务要跑 YOLO 视频检测为什么绕道 Python 调 ONNX 反而是最快的路一个 Java 后端项目里突然要接目标检测而且模型是 YOLOv5/v7/v8最快的落地方式往往不是去啃 Java 版的 ONNX 推理库而是让 Java 调 Python让 Python 用 onnxruntime 跑 ONNX 模型。我在实际项目里试过几条路最后常态选型就是这个。视频逐帧检测帧率低到每秒 5 帧都卡的话没人敢上线。合理链路是Java 从视频流取帧Python 负责 YOLO ONNX 推理和 NMS再把结果回给 Java 画框或上报业务。这样 Java 只碰视频和业务逻辑Python 只碰检测两边都不别扭。这套方案适合手里已有 Python 训练好的 YOLO 权重、需要在 Java 服务里快速出检测结果的团队也适合不想上 C、不熟悉 JNI 的 Java 工程师。2. 先定推理链路再动手Java 与 Python 的通信方式、ONNX 输出格式与常驻推理服务拿到 YOLO 模型后别急着写 Java 代码先想清楚推理链路。Java 调 Python 做视频检测最大的矛盾不是模型能不能跑而是调用开销能不能压住。视频流一秒钟 25 帧一帧推理可能只要 20 毫秒但 Java 这一侧如果搭错了传输方式单帧延迟立刻变成几百毫秒。2.1 Java 调 Python 的三种姿势视频场景为什么必须选常驻进程我见过不少人第一反应是直接用 ProcessBuilder 每帧启动一次 Python 脚本。这个做法在图片测试时没问题到了视频流就完全翻车。启动一个 Python 进程大概要 300 到 500 毫秒这里面包括解释器启动、import torch、加载 ONNX 模型、初始化 onnxruntime session。帧率算下来只有 2 FPS 上下而实际推理本身才 20 毫秒。也就是说 95% 的时间都花在了起床而不是干活上。第二种姿势是把 Python 脚本做成一个常驻进程用标准输入管道传帧。Java 启动一次 python 进程然后不断往它的 stdin 写数据从 stdout 读结果。这个方案省去了进程启动开销单帧额外成本能降到 1 到 2 毫秒但缺点是并发能力差多路视频同时检测时需要起多个 Python 进程管道协议也得自己定工程上比较别扭。第三种姿势是让 Python 起一个 HTTP 服务Java 用 HttpClient 逐帧 POST 过去。HTTP 协议本身有 3 到 5 毫秒的开销但换来的是服务可以独立启动、独立重启、天然支持多线程并发。这个方案在视频目标检测场景里是性价比最高的。如果未来多路视频并发上来这个 Python 服务还能单独部署到另一台 GPU 机器上Java 服务完全无感。三种方式对比如下调用方式单帧额外开销并发能力工程复杂度适用场景ProcessBuilder 逐帧启动300-500ms最差最低离线图片、demo 演示stdin/stdout 管道常驻1-2ms一般中单路视频、低帧率HTTP 常驻服务3-5ms好中多路视频、生产环境gRPC / 共享内存1ms最好高高帧率、低延迟要求我的建议很直接视频项目直接走 HTTP 常驻服务先跑通再谈优化。一上来就上 gRPC 或者共享内存容易在传输层浪费太多时间。2.2 ONNX 推理输出与 NMS 该放哪YOLOv5/v7 和 v8 的差异选定传输方式后还要搞明白 ONNX 模型输出的是什么。这里有个关键认知导出的 ONNX 通常只包含检测头的前半部分NMS 不包含在模型里。YOLOv5 和 YOLOv7 导出的输出 shape 一般是 [1, 25200, 85]25200 是三种尺度特征图上的候选框总数85 由 4 个坐标加 1 个 objectness 加 80 个类别分数组成。YOLOv8 则不同它没有 objectness 分支输出 shape 是 [1, 84, 8400]其中 84 是 4 个坐标加 80 个类别分数且类别分数排在坐标后面8400 是候选框数。这个差异直接决定后处理脚本怎么写如果不加判断直接解析大概率会拿到一堆乱框。NMS 放哪建议放在 Python 侧。Java 侧做 NMS 意味着要自己处理 numpy 数组、排序、IoU 计算代码量大而且容易踩坑。Python 有 numpy 和 cv2.dnn.NMSBoxes几行代码就搞定。所以在Java 调 Python这个链路里NMS 天然属于 Python。如果你看到某些导出工具支持 end2end 导出也就是把 NMS 嵌进 ONNX 模型里Java 侧确实会变简单但端到端模型的动态 shape 支持和算子兼容性在不同 YOLO 版本上并不稳定我一般不建议作为首选老老实实把原始输出拿回 Python 后处理更可控。2.3 用 Flask 搭一个最小常驻推理服务代码与关键参数一个最小可用的服务器代码长这样我用 Flask 实现模型加载一次每次请求只做预处理、推理、后处理三件事# infer_server.py import base64 import cv2 import numpy as np import onnxruntime as ort from flask import Flask, request, jsonify # 关键参数集中在这里 MODEL_PATH yolov8n.onnx INPUT_SIZE 640 # 模型输入边长 CONF_THRES 0.25 # 置信度阈值 IOU_THRES 0.45 # NMS 的 IoU 阈值 PROVIDERS [CUDAExecutionProvider, CPUExecutionProvider] session ort.InferenceSession(MODEL_PATH, providersPROVIDERS) input_name session.get_inputs()[0].name app Flask(__name__) app.route(/detect, methods[POST]) def detect(): # Java 端把帧编码成 JPEG 再 base64这里直接解码 frame_b64 request.json[frame] img cv2.imdecode( np.frombuffer(base64.b64decode(frame_b64), np.uint8), cv2.IMREAD_COLOR, ) h, w img.shape[:2] # letterbox 保持宽高比缺少这一步会导致框偏移 scale min(INPUT_SIZE / h, INPUT_SIZE / w) resized cv2.resize(img, (int(w * scale), int(h * scale))) pad_w INPUT_SIZE - resized.shape[1] pad_h INPUT_SIZE - resized.shape[0] top, bottom pad_h // 2, pad_h - pad_h // 2 left, right pad_w // 2, pad_w - pad_w // 2 blob cv2.copyMakeBorder( resized, top, bottom, left, right, cv2.BORDER_CONSTANT, value(114, 114, 114), ) # BGR - RGBHWC - CHW归一化 blob blob[:, :, ::-1].transpose(2, 0, 1)[None].astype(np.float32) / 255.0 outputs session.run(None, {input_name: blob}) pred outputs[0] # 默认 [1, 84, 8400] # 转成 [1, 8400, 84] pred np.transpose(pred, (0, 2, 1))[0] boxes pred[:, :4] class_scores pred[:, 4:] class_ids np.argmax(class_scores, axis-1) scores class_scores[range(len(class_ids)), class_ids] keep scores CONF_THRES boxes, scores, class_ids boxes[keep], scores[keep], class_ids[keep] cx, cy, bw, bh boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] x1 (cx - bw / 2 - left) / scale y1 (cy - bh / 2 - top) / scale x2 (cx bw / 2 - left) / scale y2 (cy bh / 2 - top) / scale tlwh np.stack([x1, y1, x2 - x1, y2 - y1], axis-1) indices cv2.dnn.NMSBoxes( tlwh.tolist(), scores.tolist(), CONF_THRES, IOU_THRES, ) indices np.array(indices).flatten() result [] for i in indices: x1, y1, x2, y2 ( float(x1[i]), float(y1[i]), float(x2[i]), float(y2[i]), ) result.append({ box: [x1, y1, x2, y2], score: float(scores[i]), class: int(class_ids[i]), }) return jsonify({detections: result}) if __name__ __main__: app.run(host0.0.0.0, port8001, threads8)这段代码有几个必须说清的点。PROVIDERS列表的顺序很关键onnxruntime-gpu 会优先尝试 CUDA如果你的机器没有可用的 CUDA 环境它会自动回退到 CPU不会报错但速度下降这往往是生产环境里隐性问题。INPUT_SIZE要和导出模型时的输入尺寸一致YOLOv5/v7/v8 官方默认是 640如果你导出时用了 320 或者 1280这里必须同步改。letterbox是 YOLO 系列的标配预处理很多人图省事直接cv2.resize(img, (640, 640))这会让图像变形检测框整体偏移这类问题后面避坑章还会展开。NMS 前我把坐标从中心点宽高换算成左上角宽高因为cv2.dnn.NMSBoxes接受的是[x, y, w, h]而不是[x1, y1, x2, y2]传错格式时 OpenCV 不报错但返回的框是乱的。3. YOLOv5/v7/v8 导出 ONNX导出命令、动态形状与端到端参数模型推理服务和 Java 调用代码可以放到后面写最先要做的是把 PyTorch 权重导出成干净的 ONNX。这一步看起来简单实际上导出的参数会直接影响后面的后处理复杂度。很多人卡在做完 Java 端后才发现 ONNX 输出 shape 不对、动态尺寸失灵或者带了一堆没用的输出节点回头再调导出的成本很高。3.1 YOLOv5 导出 ONNXexport.py 的核心参数YOLOv5 的官方仓库自带导出脚本在 yolov5 目录下执行命令python export.py \ --weights yolov5s.pt \ --include onnx \ --opset 12 \ --dynamic \ --simplify参数含义如下--weights指定权重文件--include onnx表示只导出 ONNX 格式--opset 12设置 ONNX 算子集版本onnxruntime 1.15 以上的环境对 opset 12 支持得很好。--dynamic导出动态尺寸默认模型输入是固定的 640x640视频流里如果以后要切换分辨率没有这个参数就得重新导出。--simplify会调用 onnx-simplifier 对计算图做一遍精简去掉多余节点部署时更稳。这个参数建议保留它不会改变模型精度但能显著减少推理框架的兼容压力。导出的 ONNX 输出 shape 是 [1, 25200, 85]这里的 85 对应 4 个坐标、1 个 objectness、80 个类别分数。坐标是相对于 640x640 输入图像的像素坐标所以在后处理里要记得除以缩放比例并减去 padding。3.2 YOLOv7 导出 ONNX注意点与输出 shape 的变化YOLOv7 的导出在 v7 仓库下命令略有不同重点在于--grid和--end2end两个开关cd yolov7 python export.py \ --weights yolov7.pt \ --grid \ --simplify \ --dynamic \ --opset 12这个项目默认导出时如果不带--gridONNX 输出里会包含中间特征图而不只是检测头的最终预测。这对部署的人来说是个大坑因为输出节点数量和多出来的特征图会让解析逻辑变得很啰嗦。加上--grid后输出会规整为 [1, 25200, 85]和 YOLOv5 一致方便共用后处理代码。--end2end我建议第一次使用时先不要开除非你确认自己需要把 NMS 也打包进模型。这套方案里我们已经在 Python 侧做了 NMS所以不需要端到端。YOLOv7 的另一个特点是对 opset 的兼容性稍差opset 12 是稳定档位opset 13 以上在 onnxruntime 低版本会报算子不支持。如果导出后加载报错优先把 opset 降到 11 再试。3.3 YOLOv8 导出 ONNX一行命令 dynamic 开关YOLOv8 的导出方式和前两个版本不太一样Ultralytics 把功能统一到了yolo命令里yolo export \ modelyolov8n.pt \ formatonnx \ opset12 \ dynamicTrue \ simplifyTrue导出后得到的模型输入是一个动态尺寸的 NCHW 张量输出 shape 默认是 [1, 84, 8400]。和 YOLOv5/v7 完全两个风格84 4 个坐标 80 个类别分数没有 objectness 那一列候选框数 8400 是由三个检测尺度累加的。正因为没有 objectnessYOLOv8 的类别分数在置信度过滤时直接看最大类别分数不需要把它乘上任何额外概率。另一个容易困惑的点YOLOv8 的分类头输出在导出时是否已经过了 sigmoid。不同 sub-version 处理不太一样我见过有的导出模型自带 sigmoid有的没有。最稳妥的判断方法是拿一张测试图用原始 PyTorch 模型推理一次再把 ONNX 的输出对比一遍如果分数差一个数量级就在 Python 后处理里对类别分数手动加一行sigmoid。这个对比不是玄学是部署 YOLOv8 绕不开的 sanity check。3.4 导出后先做单图验证别急着接入 Java不管是哪个版本导出完不要直接写 Java先用一行 Python 验证模型能正常加载和推理import onnxruntime as ort import numpy as np session ort.InferenceSession(yolov8n.onnx, providers[CPUExecutionProvider]) input_name session.get_inputs()[0].name print(input:, input_name, session.get_inputs()[0].shape) # 构造一个随机输入shape 按实际导出模型调整 x np.random.rand(1, 3, 640, 640).astype(np.float32) outputs session.run(None, {input_name: x}) for i, out in enumerate(outputs): print(foutput{i}:, out.shape, out[0, 0, :5])这一步的意义是尽早暴露问题。如果模型加载时报错说明导出环境或者算子集版本不对如果输出 shape 不是上面讲的预期值说明导出参数选错了这时候调整成本最低如果随机输入能跑通再换真实图片和 PyTorch 结果对比精度。我习惯把这一步放在每次改动模型后必跑宁可在这里多花几分钟也不要把问题带到 Java 集成阶段。4. Java 侧实现取视频帧、调 Python 服务、解析结果与性能控制Python 侧的服务跑通后Java 这边的工作就是三板斧读帧、发请求、画结果。这里面的难点不是 API 怎么调而是 Mat 的内存管理和视频帧率控制。Java 的 OpenCV 绑定不像 Python 那样自动释放对象忘掉一次 release 就是一次内存泄漏这个问题视频场景跑到十分钟后必然发作。4.1 用 OpenCV 在 Java 里逐帧取帧Java 里拉视频流常用 OpenCV 的 VideoCapture这里给一段最基础的取帧循环import org.opencv.core.Mat; import org.opencv.videoio.VideoCapture; VideoCapture cap new VideoCapture(); cap.open(/path/to/video.mp4); if (!cap.isOpened()) { throw new RuntimeException(无法打开视频文件); } Mat frame new Mat(); while (cap.read(frame)) { // 这一帧已经拿到交给检测逻辑 doDetect(frame); // 关键用完后的 Mat 要手动释放 frame.release(); } cap.release();VideoCapture.read()的行为是如果 frame 之前已经分配过内存它会在内部自动复用但 Mat 对应的原生内存不会自动归还给系统。frame.release()是每次循环结束必须调的否则 Java 堆里的 Mat 对象很小但 native 内存会一直涨最后触发 std::bad_alloc。如果是 rtsp 流cap.open的地址换成 RTSP URL其他逻辑不变但要额外注意网络断开后read()会一直返回 false需要在循环里加计数器做重连。4.2 把帧编码成 Base64 并调用推理 HTTP 接口拿到帧之后不能直接把 Mat 发给 Python需要先编码成 JPEG 字节流再 base64 放进 JSON。编码这一步用Imgcodecs.imencodeimport org.opencv.core.Mat; import org.opencv.core.MatOfByte; import org.opencv.imgcodecs.Imgcodecs; import org.opencv.imgproc.Imgproc; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.Base64; // 长边超过 640 就等比缩小减少网络传输量 double scale Math.min(1.0, 640.0 / Math.max(frame.cols(), frame.rows())); Mat sendFrame new Mat(); if (scale 1.0) { Imgproc.resize(frame, sendFrame, new org.opencv.core.Size( Math.round(frame.cols() * scale), Math.round(frame.rows() * scale))); } else { frame.copyTo(sendFrame); } MatOfByte buf new MatOfByte(); Imgcodecs.imencode(.jpg, sendFrame, buf); String base64 Base64.getEncoder().encodeToString(buf.toArray()); String json {\frame\:\ base64 \}; HttpClient client HttpClient.newHttpClient(); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(http://127.0.0.1:8001/detect)) .header(Content-Type, application/json) .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); sendFrame.release(); buf.release();注意这里我把帧先等比缩到长边 640而不是直接resize(640, 640)。直接压成正方形会让 YOLO 的输入分布和训练时不一致出现框偏移。等比缩放后Python 端的 letterbox 仍会补到 640两边都对得上。另一个重点是HttpClient要注册成一个全局单例不要每次都newHttpClient()否则连接无法复用TCP 握手开销会拖慢帧率。如果 Java 版本低于 11可以用HttpURLConnection但这个方案里 HttpClient 更顺手。4.3 结果解析与画框Java 侧只做展示不做 NMSPython 服务返回的 JSON 结构是{detections: [{box: [x1, y1, x2, y2], score: 0.9, class: 0}]}。Java 侧用 Jackson 解析然后直接画框import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.opencv.core.Point; import org.opencv.core.Scalar; import org.opencv.imgproc.Imgproc; ObjectMapper mapper new ObjectMapper(); JsonNode root mapper.readTree(response.body()); JsonNode detections root.get(detections); for (JsonNode det : detections) { double x1 det.get(box).get(0).asDouble(); double y1 det.get(box).get(1).asDouble(); double x2 det.get(box).get(2).asDouble(); double y2 det.get(box).get(3).asDouble(); int cls det.get(class).asInt(); double score det.get(score).asDouble(); Imgproc.rectangle(sendFrame, new Point(x1, y1), new Point(x2, y2), new Scalar(0, 255, 0), 2); String label String.format(class%d %.2f, cls, score); Imgproc.putText(sendFrame, label, new Point(x1, Math.max(0, y1 - 5)), Imgproc.FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(0, 0, 255), 1); }不要在这里再做 NMS。Python 已经把 NMS 做完并筛选出最终框了Java 直接消费即可。我这里画框用的sendFrame是缩放后的帧坐标直接可用。如果你业务上要给用户看原始分辨率视频就需要在发送前记录 scale然后用x1 / scale这种方式把坐标映射回原帧那就要在帧处理循环里多维护一个变量。4.4 视频流性能控制跳帧、并发与降采样视频检测最常见的性能瓶颈在 Python 服务的推理耗时而不是 Java 这边的传输。如果单帧推理稳定在 30 毫秒25 FPS 的视频理论上每秒最多处理 33 帧看起来勉强够用。但 HTTP 的序列化和等待会让实际帧率降到 15 到 20 FPS所以需要做几层性能控制。第一层是跳帧25 FPS 下可以每 2 帧检测一次中间那一帧直接复制上一次的检测结果或者干脆跳过展示。很多业务标注不需要每帧都检测5 到 10 FPS 的检测频率已经够用跳帧能把负载降一半以上。第二层是并发多路视频时每路视频一个线程Python 服务用 Flask 的threads8可以同时处理多个请求但要注意 CPU 推理时 Python GIL 会限制并行度GPU 推理时 CUDA 锁不会被 GIL 攥住并发效果更好。第三层是降采样检测分辨率 640 相比 1280推理时间能少 3 到 4 倍视频场景里物体不太小的话640 是通用选择。5. 避坑与排查Java 调 Python 做视频检测的 5 个常见问题这一章写的都是我踩过或者帮朋友排查过的真实问题。每个问题都按现象 - 原因 - 解决来写你可以把这章当成排查手册出了问题先来对照。5.1 现象每帧都启动一次 Python延迟飙到秒级很多人一开始图省事在 Java 里用 ProcessBuilder 每帧运行一次 Python 脚本。单帧处理时间直接到 700 毫秒以上视频变成幻灯片。原因Python 解释器启动、import torch、创建 onnxruntime session 都是重操作。session 创建一次大约要 100 到 200 毫秒模型大一点更慢这些开销被摊到每一帧上。解决把 Python 推理做成常驻 HTTP 服务session 全局只创建一次。Java 这边只发 HTTP 请求模型加载次数归零。如果实在不想上 HTTP也可以用 stdin/stdout 管道保住常驻进程但要注意管道读写的阻塞问题。我的建议是不要绕直接上 Flask 那套代码。5.2 现象Python 推理结果全零或者框全部偏移模型能跑但检测框和物体位置对不上或者带 conf 的输出几乎全为零。原因预处理不一致。YOLO 系列训练时用 letterbox 保持宽高比并且图像要转成 RGB、归一化到 0 到 1。如果你图省事cv2.resize(img, (640, 640))直接压成正方形或者忘记 BGR 转 RGB模型看到的输入分布和训练时完全不同输出自然全是噪声。解决严格按训练时的预处理链路走。letterbox 的 padding 值默认 114RGB 转换要在 HWC 转 CHW 之前做归一化用 255.0。另外把测试图分别过 PyTorch 和 ONNX对比检测框坐标和分数差异小于 0.01 才算对齐。这条适合在接入 Java 前先验证。5.3 现象YOLOv8 的 ONNX 输出 shape 和 YOLOv5 不一样解析直接出错按 YOLOv5 的解析代码去解析 YOLOv8 的 ONNX要么报维度错误要么画出大量不存在的框。原因YOLOv8 输出是 [1, 84, 8400]通道在第二维候选框在第三维YOLOv5/v7 是 [1, 25200, 85]候选框在第二维属性在第三维。解析前必须判断并转置。解决通用处理是在 Python 后处理开头先看outputs[0].shape。如果是[1, 84, 8400]先np.transpose(outputs[0], (0, 2, 1))转成[1, 8400, 84]如果是[1, 25200, 85]处理方式相同但后续维度索引不同。写成兼容代码时把类别数和是否含 objectness 也一起判断一个后处理函数吃三个版本是可行的。5.4 现象装了 CUDA推理速度还不如 CPU机器上有 NVIDIA 显卡装了 onnxruntime-gpu但推理延迟反而比 CPU 版还高或者提示找不到 CUDAExecutionProvider。原因常见有三种。一是pip install onnxruntime默认安装的是 CPU 版导入的是 CPU 执行引擎CUDA 根本没用上二是 CUDA、cuDNN 和 onnxruntime-gpu 的版本不匹配运行时报库加载失败三是模型太小或输入太小GPU 的显存传输和 kernel 启动开销掩盖了计算加速收益。解决先打印onnxruntime.get_available_providers()确认列表中是否有CUDAExecutionProvider。没有就卸载重装onnxruntime-gpu注意它的版本对 CUDA 有固定要求。然后在InferenceSession里显式指定 providers 顺序把 CUDA 放前面。最后如果显卡是低端型号或模型是 nano 级别CPU 线程足够多时反而更快这不算故障是计算规模不匹配。判断标准很简单用一段 5 分钟视频分别测 CPU 和 GPU 的总耗时别只看单帧。5.5 现象长时间运行后内存暴涨服务被 OOMJava 服务跑十分钟后内存占用持续上升最终 OOM 崩溃或者 Python 服务的内存也缓慢上涨。原因Java 侧 OpenCV 的 Mat 没有 release。OpenCV 的 Java 包装里 Mat 对象很小但 native 内存不受 JVM 堆控制frame.release()漏一次内存就漏一次。Python 侧则可能是每次请求里重新创建了 session或者大数组中转变量没有及时置空。解决Java 里所有Mat、MatOfByte变量用完后手动release()不要把frame.copyTo(sendFrame)这种临时 Mat 留在循环里。Python 侧把ort.InferenceSession(...)放到全局不要在detect()函数里创建。另外在 Flask 里对返回的result做一次jsonify前用del detections这类方式及时清理别在长跑时依赖 Python GC 自动回收。6. 进阶把 Python 推理做成独立服务并给这个方案做一次体检HTTP 跑通之后如果想再压一档性能可以把推理服务从 Flask 换到 gRPC。gRPC 用 protobuf 直接传二进制帧体省掉 base64 编码和 JSON 解析两层开销单帧传输成本能从 3 到 5 毫秒压到 1 毫秒以内。Java 侧用 grpc-netty 生成 stubPython 侧用 grpcio 实现 service帧体直接用bytes字段后处理结果同样走 protobuf message。这个改造的工作量不大但要求你已经把 HTTP 版本稳定跑通否则两边同步调问题会很痛苦。做完改造或优化后一定要给方案做一次体检。我习惯用一段 5 分钟的真实视频跑三组指标第一组是平均 FPS第二组是单帧延迟的 P50 和 P99第三组是长稳运行 1 小时的内存曲线。FPS 关注吞吐P99 关注卡顿内存曲线关注泄漏。压测时最好用 rtsp 推流而不是本地文件这样能暴露网络抖动和断线重连的问题。记录结果时把模型分辨率、CPU 线程数或 GPU 型号一并记录否则下次调优没有基线。这套方案我自己用下来最大的体会是分工要干净Java 不认识 YOLO 的坐标系Python 不认识业务逻辑通信只传 JPEG 和 JSON。记住一个教训不要因为 Python 推理快就在 Java 里反复横跳改架构先把 HTTP 常驻服务跑稳再谈 gRPC 和批处理。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →