资讯详情

资讯详情

Java调用Python跑YOLO ONNX视频目标检测:从部署到避坑实战

简介面向需要在 Java 后端集成深度学习目标检测能力的开发者该资源提供一套完整的 Java 调用 Python 加载 YOLO ONNX 模型实现视频目标检测与识别的解决方案。方案兼容 YOLOv5、YOLOv7、YOLOv8 等主流模型系统拆分为 Java 与 Python 双端Java 负责 RTSP/RTMP 视频流获取、帧预处理、数据传递、后处理与结果展示Python 脚本负责加载 ONNX 模型并执行推理两者通过 JNI 或类似机制协同。预处理环节包含 resize、normalization、padding并转换为 Numpy 数组以适配模型输入后处理过滤低置信度结果并绘制识别框。压缩包共 32 个文件约 144.73MB其中 12 个 Java 源文件为核心代码4 个 ONNX 模型可直接加载另有 Python 脚本、2 个 DLL 动态库、JAR 包、XML 配置、README 说明、示例图片与 MP4 演示视频目录结构清晰便于定位和移植。目前已有 506 人学习下载。通过该资源可快速搭建完整的检测链路省去自行编写跨语言调用和预处理后处理逻辑的时间适合需要落地视频流目标检测项目的研发人员参考复用。1. 为什么是 Java 调用 Python 跑 YOLO ONNX先把分工想清楚再动手Java 技术栈的项目里要接入 YOLOv5/v7/v8 的 ONNX 模型做视频目标检测与识别最省事的落地姿势往往不是用 Java 重写推理而是 Java 负责视频解码、抽帧、业务组装Python 侧用 ONNX Runtime 加载导出的 .onnx 模型对外提供 HTTP 推理接口。这套方案看起来多绕了一层网络调用实际落地却很顺模型导出、预处理、NMS 全落在 Python 生态里Java 不需要理解 anchor、stride 或 8400 个候选框怎么来的只要拿到坐标和类别就能继续做告警、画框和入库。它适合已经有 Java 视频流水线、想快速接入 YOLO 检测能力、又不想让整个团队啃 CNN 底层细节的技术团队。下面按模型导出、Python 服务、Java 调用、避坑、压测加速五步展开每一步都给可以直接复制的配置和参数。2. 先把 YOLOv5/v7/v8 统一成 ONNX导出命令与三类输出头的差异2.1 三个版本的最小导出命令与参数取舍PyTorch 模型转 ONNX最常翻车的地方不是缺依赖而是导出参数不一致。v5/v7 的导出脚本在各自仓库里v8 直接走 ultralytics 命令行。下面三条命令是项目里最常用的版本。YOLOv5 导出v6.0 及以后python export.py --weights yolov5s.pt --include onnx --img 640 640 --opset 12 --simplifyYOLOv7 导出官方 export.pypython export.py --weights yolov7.pt --grid --simplify --img-size 640 640 --batch-size 1YOLOv8 导出yolo export modelyolov8s.pt formatonnx opset12 simplifyTrue imgsz640三条命令都用了--simplify或simplifyTrue它能折叠大量常量算子让模型体积小一圈加载和解析也更快。而--img 640 640和imgsz640是刻意把输入固定成 640×640第一版部署不建议开--dynamic。动态尺寸会让输入 shape 变成 (1, 3, -1, -1)letterbox 必须跟着实际帧宽高动态算Python 和 Java 两侧都要维护同一套缩放逻辑一旦有一处没对齐坐标还原全是错的。后面排错时你先怀疑动态尺寸再怀疑模型本身效率会高很多。opset12是兼容性和算子覆盖之间的平衡点。opset 太低导出时部分算子被拆得很碎ONNX Runtime 加载反而慢opset 太高遇到旧版本 onnxruntime 可能直接报“unsupported operator”。如果部署机的 onnxruntime 版本不确定先跑一句python -c import onnxruntime; print(onnxruntime.__version__)确认再决定要不要降到 11。还有一个容易被忽略的点v7 的--grid参数决定输出是解码后的坐标网格还是原始特征图。按上面的命令导出默认输出是 (1, 25200, 85)这是后面视频对接最常见的格式。如果需要把 NMS 直接做进模型可以在 v7 命令后追加--end2end --topk-all 100 --iou-thres 0.45 --conf-thres 0.25 --max-wh 640此时输出变成 (1, 100, 6)每行是 x1, y1, x2, y2, score, class_idJava 侧拿到就能直接用代价是置信度阈值被固定进模型想调低召回率得重新导出。我一般第一版不带 end2end后处理自己写参数可调排查问题更直观。2.2 三种输出格式与 COCO80 类别读取差异把三个版本并排对比竖看这张表就够了。模型默认 ONNX 输出 shape坐标含义YOLOv5(1, 25200, 85)前 4 列 cx, cy, w, h640 尺度像素坐标第 5 列 objectness后 80 列类别得分YOLOv7非 end2end(1, 25200, 85)同 v5YOLOv7end2end(1, 100, 6)x1, y1, x2, y2, score, class_id已做完 NMSYOLOv8(1, 84, 8400)前 4 行 cx, cy, w, h0~1 归一化第 5~84 行是 80 类得分按列排布v5/v7 的 25200 是 640×640 输入下三个 stride8/16/32铺出来的候选框数量85 维是“4 个坐标 1 个置信度 80 类”。v8 去掉了 objectness 这个维度变成“4 个坐标 80 类”所以它的类别概率从第 5 个通道开始而 v5/v7 是从第 6 个通道开始。很多工程翻车就是因为拿 v5 的解析逻辑读 v8把 8400 个点的前 4 维当成了带 objectness 的坐标结果满地乱框。代码里判断输出 shape 的out.shape[1]是 84 还是out.shape[2]是 85就能安全分流。再说常听到的“coco80 怎么读”问题。COCO 80 类是固定的索引顺序0 是 person1 是 bicycle一直到 79。v5/v7 类别下标存在第 6~85 列v8 存在第 5~84 行索引顺序一致只是偏移一位。业务侧拿到 class_id 0不要自己翻译直接在 Python 服务里准备一个 COCO 标签数组推理后返回字符串 label 而不是下标避免 Java 和 Python 各自维护一份容易失配的映射表。2.3 用 ONNX Runtime 跑一次最小推理验证导出正确性导出完别急着写服务先用一段很短 Python 脚本验证模型能加载、输入输出 shape 对得上。import onnxruntime as ort import cv2 import numpy as np sess ort.InferenceSession(yolov8s.onnx, providers[CPUExecutionProvider]) name sess.get_inputs()[0].name print(输入:, [i.shape for i in sess.get_inputs()]) print(输出:, [o.shape for o in sess.get_outputs()]) image cv2.imread(test.jpg) # letterbox 到 640x640和导出 imgsz 保持一致 h, w image.shape[:2] r min(640 / h, 640 / w) nh, nw int(round(h * r)), int(round(w * r)) canvas np.full((640, 640, 3), 114, dtypenp.uint8) canvas[:nh, :nw] cv2.resize(image, (nw, nh)) inp cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB).transpose(2, 0, 1) inp inp[None].astype(np.float32) / 255.0 outputs sess.run(None, {name: inp}) print(outputs[0].shape)这段代码做完三件事把图片按比例缩放到 640、四周用 114 像素填充、转 RGB 后归一化。直接在导出脚本里顺手验证能避免把问题带到下一层。如果打印出来是 (1, 84, 8400) 或 (1, 25200, 85)说明模型输出是常规格式如果出现 (1, 100, 6)说明你导出时把 end2end 带进来了后面的后处理要走另一套逻辑。114 这个填充值是 YOLO 训练时的中间色不要随手改成 00 填充会明显拉低边框附近的识别置信度。3. Java 调用 Python用 FastAPI 包一层视频推理服务3.1 接口设计帧进 JSON 出比视频流上传更适合 Java 侧Java 是把整段视频传给 Python还是把单帧传给 Python我在生产里倾向后者。Java 本身已经在用 OpenCV/JavaCV 做视频解码抽帧节奏和时间戳掌握在 Java 手里最稳Python 端只需要接收一张 JPEG、返回坐标和类别职责单一。长视频整段上传会吃掉大量内存和带宽还要额外做断点续传和超时重试复杂度全堆到接口层。接口约定可以长这样POST /detect Content-Type: application/json { image: /9j/4AAQSk...base64..., conf_thres: 0.25, iou_thres: 0.45, max_det: 300 }返回{ boxes: [[100, 150, 220, 380]], labels: [person], scores: [0.92] }box 统一是原图坐标的 x1, y1, x2, y2不做归一化Java 侧拿到就能直接画框入库。JPEG 转 base64 有约 33% 的体积膨胀但一张 1080p 帧压缩后通常在 100~300KB 内局域网里影响很小真要跨公网调用可以把决体改成二进制 body 或 gRPC接口语义不用动。这里要劝退一种做法别用 Java 的 ProcessBuilder 每帧去起一个 Python 进程。解释器初始化就要几百毫秒GIL 一锁单路视频都跑不满。Python 推理必须做成长驻服务模型加载一次后续请求只做推理。这也是“Java 调用 Python”这句话最正确的落地姿势调用的是服务不是进程。3.2 Python 服务代码预加载模型、统一处理 v5/v7/v8 输出FastAPI 很适合这个场景自带请求校验pydantic 能少写一堆防御代码。模型在模块加载时创建 ONNX Session不要在请求里重复初始化。from fastapi import FastAPI, HTTPException from pydantic import BaseModel import base64 import cv2 import numpy as np import onnxruntime as ort # 按 COCO80 顺序补齐 80 个标签这里省略中间部分 COCO_CLASSES [person, bicycle, car, motorcycle, airplane, bus, train, truck, boat] [] * 71 class FrameRequest(BaseModel): image: str conf_thres: float 0.25 iou_thres: float 0.45 max_det: int 300 app FastAPI() # 优先 GPU没有则回退 CPU if CUDAExecutionProvider in ort.get_available_providers(): providers [CUDAExecutionProvider, CPUExecutionProvider] else: providers [CPUExecutionProvider] sess ort.InferenceSession(yolov8s.onnx, providersproviders) input_name sess.get_inputs()[0].name def letterbox(img, size640): h, w img.shape[:2] r min(size / h, size / w) nh, nw int(round(h * r)), int(round(w * r)) resized cv2.resize(img, (nw, nh)) canvas np.full((size, size, 3), 114, dtypenp.uint8) canvas[:nh, :nw] resized return canvas, r def postprocess(out, ratio, conf_thres, iou_thres, max_det): # out 可能是 (1, 84, 8400) 或 (1, 25200, 85) out out[0] if out.shape[0] 84: # YOLOv8坐标是 0~1 归一化需要乘回 640 boxes out[:4].T * 640 probs out[4:].T else: # YOLOv5/v7 默认输出坐标已是 640 尺度像素 boxes out[:, :4] obj out[:, 4:5] cls out[:, 5:] probs obj * cls scores probs.max(axis1) classes probs.argmax(axis1) keep scores conf_thres boxes, scores, classes boxes[keep], scores[keep], classes[keep] if len(scores) 0: return [], [], [] cx, cy, bw, bh boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] x1, y1, x2, y2 cx - bw / 2, cy - bh / 2, cx bw / 2, cy bh / 2 idx cv2.dnn.NMSBoxes( np.stack([x1, y1, x2, y2], axis1).tolist(), [float(s) for s in scores], conf_thres, iou_thres ) if idx is None or len(idx) 0: return [], [], [] keep_idx idx.flatten() order keep_idx[np.argsort(scores[keep_idx])[::-1][:max_det]] boxes_out, labels_out, scores_out [], [], [] for i in order: boxes_out.append([ int(x1[i] / ratio), int(y1[i] / ratio), int(x2[i] / ratio), int(y2[i] / ratio) ]) labels_out.append(COCO_CLASSES[int(classes[i])]) scores_out.append(float(scores[i])) return boxes_out, labels_out, scores_out app.post(/detect) def detect(req: FrameRequest): try: buf np.frombuffer(base64.b64decode(req.image), dtypenp.uint8) img cv2.imdecode(buf, cv2.IMREAD_COLOR) if img is None: raise HTTPException(status_code400, detailbad image) canvas, ratio letterbox(img) inp cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB).transpose(2, 0, 1) inp inp[None].astype(np.float32) / 255.0 out sess.run(None, {input_name: inp})[0] boxes, labels, scores postprocess( out, ratio, req.conf_thres, req.iou_thres, req.max_det ) return {boxes: boxes, labels: labels, scores: scores} except HTTPException: raise except Exception as e: raise HTTPException(status_code500, detailstr(e))这段代码最关键的是 postprocess 的分流逻辑。v8 输出第一维是 84坐标是 0~1 归一化乘以 640 才能得到 letterbox 像素坐标v5/v7 默认输出是 (1, 25200, 85)坐标已经是 640 尺度不能再乘一次。v5/v7 这里用 objectness 乘类别概率得到最终置信度v8 没有 objectness直接取 80 类概率的最大值。如果你把 v8 的 0~1 坐标直接当像素坐标用检出的框会小几十倍肉眼看上去就是一堆点。conf_thres控制最低置信度默认 0.25 适合从视频里找目标如果业务要求更高的准确率、能容忍漏检调到 0.4 效果更干净。iou_thres是 NMS 的 IoU 阈值默认 0.45目标密集场景人群、货架降到 0.3 能减少框之间互相吞并。max_det限制单帧最大检测数防止车辆密集路段一次返回几百个框把 Java 侧处理打垮。3.3 边界权衡为什么通常不让 Java 直接调 onnxruntime-java读者一定会问既然 Java 也有 ONNX Runtime 官方绑定为什么还要绕 Python答案是预处理和后处理。Java 直调 ONNX Runtime 时letterbox、BGR/RGB 转换、归一化、坐标还原都要在 Java 里重新实现而 OpenCV Java 的NMSBoxes接口在 4.5.x 之后才稳定可用版本一乱解析代码就跟不上。更现实的问题是模型迭代v8 出来输出头变了Java 端要重新写解析还要处理打包、依赖冲突、内存释放投入产出比很低。Python 服务独占模型推理Java 只消费 JSON。模型换版本、换导出参数、换量化方式影响的只有 Python 服务内部Java 侧代码几乎零改动。这个边界一旦划清楚后续团队里有人想试 v9 或者换 TensorRT 后端都不需要动 Java 工程。延迟上局域网内一次 HTTP 调用约 1~5ms相比 YOLOv8s 在 CPU 上 30~80ms 的推理耗时网络开销不是瓶颈。真到了每路视频要跑 30FPS 推理的极端场景再考虑 Java 直调也不迟。4. Java 侧的视频对接抽帧、HTTP 调用与时间轴回填4.1 用 JavaCV 逐帧抽取视频并按参数抽帧Java 侧用 JavaCV 的 FFmpegFrameGrabber 读视频。选它而不是原生 OpenCV是因为 FFmpeg 对 mp4/h264/hevc 的解码兼容性更好碰到损坏文件能容错不会整个进程挂掉。抽取帧的逻辑很简单以源视频帧率为基准按目标检测帧率间隔抽帧。import org.bytedeco.javacv.FFmpegFrameGrabber; import org.bytedeco.javacv.Frame; import org.bytedeco.javacv.Java2DFrameConverter; FFmpegFrameGrabber grabber new FFmpegFrameGrabber(videoPath); grabber.start(); double srcFps grabber.getFrameRate(); double detectFps 5.0; int interval (int) Math.max(1, Math.round(srcFps / detectFps)); long frameNo 0; Frame frame; while ((frame grabber.grabImage()) ! null) { if (frameNo % interval 0) { byte[] jpg frameToJpeg(frame); // 把 jpg 交给推理线程或直接同步调用 } frameNo; } grabber.stop();grabImage()返回的是解码后的图像帧frameNo % interval 0表示每当累积到间隔数就送检一帧。这样写之后你仍然会解码视频的每一帧但推理只做目标帧解码开销理论上省不掉如果连解码都想省就改用按时间戳 seek 的方式抽关键帧代价是很多检测目标会被跳过去。帧转 JPEG 的代码注意 Java2DFrameConverter 不可多线程共享private byte[] frameToJpeg(Frame frame) throws IOException { Java2DFrameConverter converter new Java2DFrameConverter(); BufferedImage bi converter.convert(frame); ByteArrayOutputStream baos new ByteArrayOutputStream(); ImageIO.write(bi, jpg, baos); return baos.toByteArray(); }这段代码会对每帧做一次 BufferedImage 转换色彩空间从 BGR 到 sRGB 可能有轻微偏移对检测影响通常不大。如果你发现颜色偏移影响了识别改用 OpenCV 的 Mat 和Imgcodecs.imencode(.jpg, mat, buf)那个路径不经过 Java2D 配色保真度更高。另外JavaCV 的本地库版本要和部署机匹配常见翻车就是本机跑得好好的部署机报NoClassDefFoundError本质是ffmpeg二进制没放对位置打包时把javacpp-platform依赖带进发行物就好。4.2 Java 用 HttpClient 调用 Python 推理服务并解析结果Java 11 以上直接用java.net.http.HttpClient不用再引 OkHttp。有个关键经验客户端和 ObjectMapper 都做成单例不要在每帧请求里重建。代码结构大概是public class YoloClient { private final HttpClient client; private final ObjectMapper mapper; private final URI uri; public YoloClient(String endpoint) { this.uri URI.create(endpoint); this.client HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) .build(); this.mapper new ObjectMapper(); } public DetectionResult detect(byte[] jpg, double conf, double iou) throws IOException, InterruptedException { String payload mapper.createObjectNode() .put(image, Base64.getEncoder().encodeToString(jpg)) .put(conf_thres, conf) .put(iou_thres, iou) .put(max_det, 300) .toString(); HttpRequest req HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(5)) .header(Content-Type, application/json) .POST(HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponseString resp client.send(req, HttpResponse.BodyHandlers.ofString()); if (resp.statusCode() ! 200) { throw new IOException(detect failed: resp.statusCode()); } return mapper.readValue(resp.body(), DetectionResult.class); } }DetectionResult是一个 POJO字段写ListListInteger boxes、ListString labels、ListDouble scoresJackson 自动映射。超时时间按实际场景调局域网内 5 秒绝对够如果 Python 服务偶尔因 GPU 冷启动变慢把超时放到 10 秒并增加重试但重试只适用于重复帧不要在视频流中盲目重试同一帧容易把队列堵死。base64 解码和 JSON 序列化的 CPU 开销在每帧约几毫秒比推理耗时低一个量级不用过分优化。真正要优化的是连接复用HttpClient 默认会复用连接只要不手动 close 连接池持续发送没有问题。4.3 时间轴回填与结果落库、画框视频检测和图片检测最大的差别就是时间轴。推理结果必须能回退到源视频的某个时间戳上否则后面做事件检索、片段剪辑、轨迹追踪全部对不上。JavaCV 里grabber.getTimestamp()返回当前帧的微秒时间戳抽帧时直接取record FrameTask(long timestampUs, byte[] jpg) {} record DetectedEvent(long timestampUs, String label, float score, int[] box) {}如果你把推理丢进异步线程池时间里要作为参数传进任务绝不能到回调里再调grabber.getTimestamp()——那时候已经读到后面的帧了。这个坑我同事踩过N次异步回来取的是送测后第二、三帧的时间戳结果一大批检测事件被标到了未来时间检索出的片段全都错位。生产里我习惯把抓帧和推理解耦。抓帧线程只管从视频里读帧、压缩成 JPEG、放进ArrayBlockingQueue固定 2~4 个消费线程从队列取帧调 YoloClient 推理按时间戳写入有序结果集。队列要有界比如 32满了就丢帧而不是阻塞读帧线程。抽帧速率一旦超过推理吞吐丢帧是最安全的退避策略不会拖垮整条视频流水线。检测结果落库时除了 label、score、box还要存视频源 ID 和时间戳范围这样后续按“某路摄像头 5 分钟内出现的 person”聚合查询才有索引可用。画框一般放在回放端做Java 侧只存坐标不在服务端渲染视频省下大量 CPU 开销。5. 避坑清单五种常见翻车场景与排查路径5.1 v8 的输出头不适用 v5 的后处理现象同一套解析逻辑在 v5 上检得好好的换成 v8 后结果全是乱框置信度虚高位置离谱。原因v5/v7 输出是 (1, 25200, 85)类别从第 6 列开始v8 输出是 (1, 84, 8400)类别从第 4 行开始坐标还是 0~1 归一化。用 v5 的逻辑读 v8等于把类别概率当成了坐标后处理全错。解决代码里按out.shape[1] 84还是out.shape[2] 85分派后处理不要靠文件名后缀猜模型版本。导出后先跑最小验证脚本打印 shape再把 shape 分支写死。以后换模型版本第一件事就是看输出 shape 是否变化。5.2 Java 传大帧导致超时或内存不足现象单路 1080p 检测正常多路并发时 Java 堆内存飙升Python 服务偶尔 5 秒超时甚至返回内存错误。原因原始帧 JPEG 过大base64 又膨胀 33%Python 端解码后一帧 1080p 占用约 6MB 内存多路并发乘上请求排队数很容易把容器内存打爆。解决Java 端在送测前先压缩最长边限制到 1280 或 960JPEG 质量不必用 10085 对检测影响很小。Python 端用uvicorn启动时加上参数限制请求体大小同时在 FastAPI 的入口做解码失败兜底。多路场景先从 960px 开始压测再逐步放大看阈值。5.3 开了动态尺寸导致 letterbox 对不齐现象v8 加了--dynamic后同一张图不同尺寸输入检测结果时好时坏小目标经常丢。原因动态尺寸下letterbox 的缩放比例随输入变化坐标还原必须实时按输入尺寸计算推理尺寸和训练尺寸差太远小目标特征被过度压缩。解决第一版固定imgsz640。如果业务需要多分辨率输入至少固定 batch 维坐标计算统一用 640 尺度Java 侧在发帧前先把帧缩放编码。我在生产里基本不会让模型在手机上跑动态尺寸省下的内存抵不掉排错成本。5.4 同步调用撑不起多路视频现象单路视频稳稳的加到第四路时响应时间直线上升甚至线程池拒绝任务。原因同步调用时每个视频流的一帧都占着一个线程在等 HTTP 响应线程池耗尽系统吞吐立刻崩。解决抓帧和推理解耦抓帧线程只入队消费线程按固定个数跑同时算好总负载路数 4、每路检测 5 帧/秒就是 20 FPS 推理负载。如果 640×640 的 YOLOv8s 在 CPU 上单核只能跑 15 FPS那就得降 detectFps 或上 GPU不能靠无限加线程硬扛。5.5 首次推理耗时异常启动后慢十几秒现象模型加载后第一个请求响应 5~10 秒后续请求恢复正常但监控里已经报了两次“请求超时”。原因ONNX Runtime 首次调用会做 shape 推导和内存分配GPU 版的 CUDA context 初始化更慢。解决服务启动后立刻用一张 640×640 全黑图跑一次推理也就是常见的 warm-up。Java 侧启动完成再优雅放流量可以通过一个/health接口确认 Python 服务 warm-up 完成后再对外提供服务。这个细节不在模型代码里但在生产环境里非常重要。6. 进阶技巧先把单路延迟和多路容量的账算清再做优化视频检测压测前先算一笔简单账一路 1080p25 的视频业务往往不需要每帧都检测。检测帧率定为 5 FPS也就是每 5 帧检 1 帧已经能覆盖大多数行人、车辆的运动规律。多路总负载的估算公式是总检测负载 路数 × 检测FPS。以 640×640 的 YOLOv8s 为例一张 T4 级别的 GPU 用 CUDA 执行器跑吞吐可以到几百 FPS撑几十路 5FPS 需求并不吃力纯 CPU 会先成为瓶颈优先考虑 ONNX 的 int8 量化量化后推理延迟大约降到原来的三分之一代价是精度可能小幅下降必须用校准集评估。想进一步提高吞吐把 Python 服务改成一次接受一组帧也就是批推理。请求体变这样{ images: [base64_1, base64_2, base64_3, base64_4], conf_thres: 0.25, iou_thres: 0.45 }服务端把多张图堆成(4, 3, 640, 640)的 batch 一次运行GPU 利用率会明显提升CPU 上 batch 收益相对小而且显存内存占用变大。Java 侧攒够 4 帧再发一次请求剩余帧丢弃延迟仍然可控。批大小建议从 4 开始压测观察帧率和内存变化再调整。验证优化到底值不值我习惯用同一段 60 秒视频做 A/B 对比记录三个指标检测出的目标总数、平均置信度、单帧耗时。优化后目标数和置信度比基线差超过 5%就说明精度退化明显这个优化建议回退。量化、抽帧、压缩这三板斧各有代价先看瓶颈在哪再动手推理慢就量化网络慢就压帧整条链路不稳才降检测帧率。我现在拿到 Java 视频项目第一件事就是确认模型版本和导出设置把输出 shape 对上号再定检测帧率和帧尺寸。这个顺序确实能避掉大多数返工也让后面接手的同事少靠猜。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

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

立即咨询 →