ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Yolov5目标检测Web部署实战:Flask框架从封装到生产落地

Yolov5目标检测Web部署实战:Flask框架从封装到生产落地 简介面向需要在Web端快速上线YOLOv5目标检测能力的开发者这份资源提供了一个完整的Flask部署项目覆盖模型加载、图片上传、推理结果JSON化及简单前端交互适合具备基础Python和深度学习概念、希望避开从零搭建环境的学习者。压缩包共15个文件约187KB其中Python脚本4个承担主服务与推理流程Dockerfile方便容器化部署HTML/CSS构成可用的上传页面README和Markdown说明辅助快速上手。目前已有713人学习下载。资源除包含可直接运行的Flask服务示例与REST接口外还附带测试脚本、示例图片和依赖清单能够帮助读者理解YOLOv5模型如何与Flask路由结合并掌握从本地调试到云端部署的关键步骤。整体目录结构简洁适合作为课程设计、技术验证或企业内部演示项目的起步模板。1. Yolov5目标检测web部署flask框架为什么这是最省事的落地路线训练好的Yolov5模型躺在电脑里别人没法用这是算法工程师和运维同学最常见的尴尬。用Flask把Yolov5包成一个HTTP服务别人只要打开浏览器上传一张图片就能拿到检测结果框不需要装Python环境、不需要跑推理脚本。这个组合之所以流行是因为Yolov5的推理逻辑和Flask的路由分发天然契合——推理慢就异步化显存不够就多进程共享前端要展示就把Base64图片塞进JSON里。适合谁训练过Yolov5但没做过服务化的算法人员以及要快速给业务方交付检测能力的后端工程师。本文按我自己的部署路径来写从推理代码封装、Flask接口设计、前端交互到生产环境的多进程坑每一步都有可复制的代码和参数。2. 把Yolov5模型包成Flask服务推理代码分离与最小可用接口2.1 封装推理类模型加载一次接口调用多次Yolov5官方仓库的detect.py是给命令行用的直接拿来当服务用会有两个问题每来一个请求就加载一次模型显存扛不住日志会刷屏而且输出的结果格式不适合HTTP返回。我一般会单独写一个推理类把模型加载、预处理、后处理全封装进去。先看加载和推理部分import torch import cv2 import numpy as np from pathlib import Path class Yolov5Detector: def __init__(self, weights_path: str, conf_thres: float 0.25, iou_thres: float 0.45, device: str cpu): # 加载Yolov5官方仓库的模型weights可以是.pt文件 self.model torch.hub.load(ultralytics/yolov5, custom, pathweights_path, force_reloadFalse) # 推理参数置信度阈值和NMS的IOU阈值 self.model.conf conf_thres # 低于该置信度的框直接丢弃 self.model.iou iou_thres # NMS时两个框重叠超过该值保留分数高的 # 设备选择有GPU就用cuda否则CPU self.device device if device cuda and torch.cuda.is_available(): self.model.cuda() else: self.model.cpu() def detect(self, image: np.ndarray): 输入BGR图片OpenCV默认格式返回检测结果 每个结果包含box坐标(x1,y1,x2,y2)、置信度、类别id、类别名 results self.model(image, size640) # size是输入网络的缩放尺寸 # 转成pandas DataFrame方便后续提取数据 df results.pandas().xyxy[0] boxes [] for _, row in df.iterrows(): boxes.append({ x1: int(row[xmin]), y1: int(row[ymin]), x2: int(row[xmax]), y2: int(row[ymax]), conf: round(float(row[confidence]), 4), class: int(row[class]), name: row[name] }) return boxes这段代码有几个关键参数需要说明。conf_thres是置信度阈值默认0.25业务上如果要求“宁缺毋滥”就调到0.4以上如果要求“别漏检”就降到0.1但误检会明显增加。iou_thres是NMS的IOU阈值默认0.45检测密集小目标时比如人群、货架上的商品要降到0.3以下否则相邻框会被合并掉。size640是输入网络的缩放尺寸Yolov5官方训练默认就是640如果你训练时用的是1280这里必须改成1280否则精度会明显下降。推理类这样封装的另一个好处是如果以后要换Yolov8或者换ONNX模型只需要改这一个类Flask路由不用动。2.2 Flask最小接口POST接收图片返回JSON结果有了推理类Flask这边就简单了。核心思路是全局只初始化一次Detector每个请求直接复用避免重复加载模型。看下面的代码from flask import Flask, request, jsonify import cv2 import numpy as np app Flask(__name__) # 全局初始化一次进程内只加载一次模型 # 生产环境建议用环境变量传参别把路径写死 detector Yolov5Detector( weights_path./weights/best.pt, conf_thres0.35, # 业务侧要求误检率低阈值适当调高 iou_thres0.45, devicecuda # 服务器有显卡就用cuda没有就改成cpu ) app.route(/detect, methods[POST]) def detect(): # 接收前端上传的图片文件 file request.files.get(image) if file is None: return jsonify({code: 400, msg: 缺少image字段}), 400 # 读入图片先读字节流再转成OpenCV的BGR格式 img_bytes file.read() nparr np.frombuffer(img_bytes, np.uint8) img cv2.imdecode(nparr, cv2.IMREAD_COLOR) if img is None: return jsonify({code: 400, msg: 图片解码失败请检查格式}), 400 # 调用推理 boxes detector.detect(img) # 返回JSON前端拿坐标去画框 return jsonify({ code: 200, count: len(boxes), boxes: boxes }) if __name__ __main__: # debug模式只用于本地调试生产环境禁用 app.run(host0.0.0.0, port5000, debugFalse)这个接口的设计有几个细节值得注意。第一用request.files.get(image)而不是request.json因为图片用multipart/form-data传输更高效前端也更好构造。第二cv2.imdecode读取的是BGR格式而Yolov5内部处理时用的是RGB但torch.hub.load加载的Yolov5模型内部已经做了通道转换所以直接传BGR图片进去没问题。第三返回结构里加了一个count字段前端在拿到结果后可以先判断有没有检测到目标再决定要不要画框。接口跑起来后可以用curl快速验证curl -X POST http://localhost:5000/detect \ -F imagetest.jpg \ -o result.json如果result.json里能正常输出boxes数组说明最小闭环已经跑通了。注意这里我特意没有返回带标注的图片因为第一个版本先让接口干净只负责出数据。2.3 返回带框图片图床方案还是Base64直传只返回坐标的话前端还得自己加载原图画框有些场景下比如前端是纯展示的大屏反而麻烦。另一种常见做法是服务端直接把标注过的图片返回。这个方案有两种实现路径一是把画好框的图片存到服务器静态目录返回一个URL二是直接把图片Base64编码后塞进JSON返回。app.route(/detect_with_img, methods[POST]) def detect_with_img(): file request.files.get(image) if file is None: return jsonify({code: 400, msg: 缺少image字段}), 400 img_bytes file.read() nparr np.frombuffer(img_bytes, np.uint8) img cv2.imdecode(nparr, cv2.IMREAD_COLOR) # 推理并画框 boxes detector.detect(img) for box in boxes: x1, y1, x2, y2 box[x1], box[y1], box[x2], box[y2] # 画矩形框和标签背景 cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) label f{box[name]} {box[conf]:.2f} cv2.putText(img, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) # 方案一编码为Base64直接返回 _, buffer cv2.imencode(.jpg, img) img_b64 base64.b64encode(buffer).decode(utf-8) # 方案二存静态目录返回URL需要先配置static_folder # save_path f./static/result_{int(time.time())}.jpg # cv2.imwrite(save_path, img) # return jsonify({code: 200, image_url: f/static/result_xxx.jpg}) return jsonify({code: 200, image_base64: img_b64, boxes: boxes})Base64方案的好处是不用管静态文件的清理问题——存了文件就得定期清理否则磁盘迟早塞满坏处是图片体积会膨胀约33%如果原图2MB返回的JSON里光图片就有2.7MB请求响应会变慢。我的建议是内部调试用Base64方便看效果对外正式交付用URL方案同时写一个定时任务清理超过10分钟的临时文件。另外要注意cv2.imencode(.jpg, img)用JPEG压缩后透明通道和部分颜色信息会有损失如果业务上需要精确标注可以改用.png格式。3. 前端交互与检测展示从单张上传到摄像头实时检测3.1 用HTML模板搭一个图片上传与结果展示页Flask自带render_template渲染HTML不需要单独写前端服务。我先写一个最简单的页面用户选择图片上传前端用fetch把图片POST到/detect_with_img接口拿到Base64图后直接替换img标签的src。看一下模板的关键部分!DOCTYPE html html head meta charsetutf-8 titleYolov5检测Demo/title /head body h2上传图片进行目标检测/h2 input typefile idimageInput acceptimage/* button onclickuploadImage()开始检测/button div idresult img idresultImg stylemax-width: 800px; display: none; pre idresultInfo/pre /div script async function uploadImage() { const fileInput document.getElementById(imageInput); if (!fileInput.files.length) { alert(请先选择图片); return; } const formData new FormData(); formData.append(image, fileInput.files[0]); const resp await fetch(/detect_with_img, { method: POST, body: formData }); const data await resp.json(); if (data.code 200) { document.getElementById(resultImg).src data:image/jpeg;base64, data.image_base64; document.getElementById(resultImg).style.display block; document.getElementById(resultInfo).textContent 检测到 data.count 个目标\n JSON.stringify(data.boxes, null, 2); } else { alert(data.msg); } } /script /body /htmlFlask侧的路由也简单from flask import render_template app.route(/) def index(): return render_template(index.html)这里有几个前端细节要注意。第一acceptimage/*只是给用户一个文件选择过滤并不代表后端只接受图片——后端还是得做校验和后处理防御不能因为前端限制了就放松。第二fetch上传时不要手动设置Content-Type让浏览器自动生成带boundary的multipart/form-data如果手动设成application/jsonFlask的request.files就取不到内容了。第三data:image/jpeg;base64,这个前缀必须有否则浏览器不会把Base64字符串当图片渲染。第一次跑通这个流程后你会得到一个“选图→点按钮→看结果”的完整闭环。3.2 摄像头实时检测WebRTC推流还是定时抓帧单张图片上传只适合“检测一次”的场景如果要连续检测比如车间监控、仓库入口就得换思路。摄像头实时检测最常见的方案不是WebRTC而是“前端定时抓帧后端逐帧推理”前端用getUserMedia打开摄像头通过canvas把当前帧转成Blob每秒发送1~2次到后端。这种方式实现简单而且Yolov5处理一帧640x640图片在GPU上大约需要20~50msCPU上要300ms以上本身就是性能瓶颈没必要做高帧率推流。// 打开摄像头 const video document.getElementById(video); navigator.mediaDevices.getUserMedia({ video: true }) .then(stream { video.srcObject stream; }); // 定时抓帧并发送检测 let timer null; function startDetect() { if (timer) clearInterval(timer); timer setInterval(async () { const canvas document.createElement(canvas); canvas.width video.videoWidth; canvas.height video.videoHeight; const ctx canvas.getContext(2d); ctx.drawImage(video, 0, 0); const blob await new Promise(resolve canvas.toBlob(resolve, image/jpeg, 0.7)); const formData new FormData(); formData.append(image, blob, frame.jpg); const resp await fetch(/detect_with_img, { method: POST, body: formData }); const data await resp.json(); // 把检测结果画到一个叠加canvas上 drawOverlay(data.boxes); }, 1000); // 每秒一帧根据模型速度调整 }这个方案有性能上的坑我在这里说清楚。浏览器canvas.toBlob转JPEG需要几十毫秒网络传输和Flask解析也需要时间所以前端定时器的间隔至少要留500ms以上否则前一个请求还没回来下一个又发出去会造成请求堆积。实测经验是GPU服务器上间隔800ms~1000ms比较合适CPU上间隔要调到2000ms以上。另外一个隐蔽问题是canvas.toBlob的压缩质量参数0.7越低图片越小传输越快但后端cv2.imdecode出来的图片会更模糊目标太小的时候会直接漏检所以质量参数不要低于0.6。3.3 前端画框的坐标换算服务器画好还是前端画两个方案前面都提到了——服务器端画框返回图片或者前端拿坐标自己在canvas上画。服务器端画框的好处是简单直接返回的图就是最终结果适合做截图留证坏处是传回一张完整图片的带宽成本高每帧2MB在局域网还能接受公网就会卡。前端画框的好处是只传坐标数据量只有几百字节而且可以灵活切换“显示检测框”和“隐藏检测框”坏处是坐标需要做一次缩放换算。function drawOverlay(boxes) { const overlay document.getElementById(overlay); const ctx overlay.getContext(2d); ctx.clearRect(0, 0, overlay.width, overlay.height); // video元素的显示尺寸和视频实际尺寸可能不同需要换算 const scaleX overlay.width / video.videoWidth; const scaleY overlay.height / video.videoHeight; boxes.forEach(b { const x1 b.x1 * scaleX, y1 b.y1 * scaleY; const x2 b.x2 * scaleX, y2 b.y2 * scaleY; ctx.strokeStyle #00ff00; ctx.lineWidth 2; ctx.strokeRect(x1, y1, x2 - x1, y2 - y1); }); }这里最容易翻车的点在于overlay画布的大小和video元素在页面上展示的大小不一致。如果video标签的CSS宽度是480px但摄像头原始分辨率是1280x720直接把Yolov5返回的坐标画上去框的位置会偏。坑在于Yolov5推理时会把原图按比例缩放到640x640实际上是letterbox填充所以返回的x1, x2等坐标已经换算回原始分辨率了前端要做的只是把“原始分辨率坐标”映射到“页面显示分辨率”。换算方式用上面的scaleX和scaleY就行如果页面要求保持宽高比可以只按一个方向缩放另一个方向居中偏移。4. 生产部署Gunicorn多进程下的显存翻车现场4.1 为什么app.run()不能用于生产环境Flask自带的服务器是单进程单线程的开发时跑一跑没问题一旦有多个请求同时进来比如前端每秒发两帧或者多人同时上传图片就会阻塞。常见做法是换GunicornLinux下或WaitressWindows下作为WSGI服务器。我习惯用Gunicorn启动命令如下# 4个worker进程每个进程独立加载一次模型 # 线程数设为2因为PyTorch推理时多线程会争抢GIL线程多了反而慢 gunicorn -w 4 -b 0.0.0.0:5000 -t 120 app:app这个命令会在4个worker进程里各跑一个Flask实例也各加载一份模型。听起来很合理但实际部署时第一个坑就来了如果你的模型文件是Yolov5s约14MB4个进程加载4份显存占用大概是模型推理显存约2GB的4倍8GB的显卡直接OOM。更隐蔽的是Gunicorn的worker进程是fork出来的而PyTorch在加载模型时会申请CUDA context多个进程各自持有独立的CUDA context显存不能共享。我在第一次部署时就在这里翻过车现象是服务启动后前几个请求正常到第3个请求就报CUDA out of memory。4.2 显存控制减少Worker数量还是改用预加载解决Gunicorn多进程显存爆炸的思路有几个方向。第一个方向是减少worker数量-w 2甚至-w 1但这样并发能力又没了。第二个方向是Gunicorn的--preload参数# 先加载模型再fork子进程子进程继承父进程的模型 gunicorn -w 4 --preload -b 0.0.0.0:5000 -t 120 app:app--preload的原理是Gunicorn先启动一个主进程导入你的app.py模块这时模型加载了一次然后再fork出4个worker子进程。Linux的fork在写时复制机制下子进程会共享父进程的内存页——但CUDA显存不走这个机制所以这个参数只能缓解CPU内存的重复占用对显卡OOM帮助不大。真正治本的做法是我现在最常用的把模型放在一个独立的常驻进程中Flask进程通过本地IPC去调用推理结果。但如果你不想引入额外的服务组件有一个折中方案把worker数调成和显卡能承受的数量一致并用环境变量控制每个进程的推理并发。第三个方向是给模型加载加一个全局锁让同一进程内的推理串行化。Yolov5的推理在GPU上虽然有线程安全保护但在同一进程内开多线程推理同一个模型实例CUDA context会被争抢速度反而会下降还可能报CUDA error: device-side assert triggered。我在代码里用threading.Lock()包住推理调用import threading class Yolov5Detector: def __init__(self, ...): self._lock threading.Lock() # ... 模型加载略 def detect(self, image): with self._lock: results self.model(image, size640) # 后处理可以不持锁因为pandas转换只涉及CPU df results.pandas().xyxy[0] # ... 转换略注意锁的范围推理部分必须持锁但后处理pandas().xyxy[0]和遍历可以放锁外因为这些操作是纯CPU的数据转换不会碰CUDA context。这样设计后Gunicorn每个worker即使配了2个线程实际上推理还是串行的但HTTP解析和JSON序列化可以并行性能损失并不大。4.3 用Docker固定部署环境CUDA、PyTorch与Flask版本一致性Yolov5这种项目最怕“本地能跑服务器跑不起来”的玄学问题——本质是CUDA、PyTorch、Python三者的版本矩阵太复杂。我的做法是直接打包成Docker镜像把环境一次固定住。一个可用的Dockerfile长这样FROM pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime WORKDIR /app # 安装Yolov5依赖官方requirements.txt COPY requirements.txt . RUN pip install -r requirements.txt # 安装Flask和Gunicorn RUN pip install flask gunicorn # 拷贝项目代码和训练好的模型权重 COPY app.py . COPY templates/ ./templates/ COPY weights/ ./weights/ EXPOSE 5000 CMD [gunicorn, -w, 2, -b, 0.0.0.0:5000, -t, 120, app:app]构建和启动docker build -t yolov5-flask:latest . docker run -d --name yolov5-server --gpus all -p 5000:5000 yolov5-flask:latestDocker方案里有几个参数需要注意。--gpus all是NVIDIA Container Toolkit的写法如果服务器没装nvidia-docker容器里即使装了CUDA也用不了GPU。pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime这个基础镜像自带CUDA和PyTorch不需要再单独装NVIDIA驱动——宿主机只要驱动版本足够新就行。-w 2是worker数Docker容器里根据显卡显存调整RTX 3090跑Yolov5s的话2~3个worker比较稳再多就OOM。还有一个容易忽略的点是-t 120超时时间Yolov5的模型加载本身就要2~5秒如果请求排队时间较长Gunicorn默认的30秒超时会直接把worker杀掉然后所有用户看到502。120秒是个稳妥值但也别设太大否则请求积压时用户要等2分钟才看到错误体验更差。5. 部署避坑Yolov5Flask最常见的5个坑5.1 图片解码失败但前端显示正常现象前端上传一张手机拍的HEIC格式图片后端返回“图片解码失败”。原因cv2.imdecode不支持HEIC格式只支持JPEG、PNG、BMP等常规格式。手机相册默认导出的可能是HEIC浏览器的input typefile不会转换格式原始字节流直接传到后端就翻车了。解决在Flask侧增加格式兜底——先尝试cv2.imdecode失败后用PIL的Image.open再试一次PIL支持更多格式然后再转成BGR。实测PIL读不了所有HEIC需要安装pillow-heif扩展但至少能覆盖大部分场景。最稳妥的做法是在前端就做好转换上传前用canvas把图片转成JPEG再发送我在摄像头检测方案里已经这么做了单张上传也建议照搬。5.2 多进程下显存OOM但单进程正常现象本地python app.py跑得好好的一上Gunicorn-w 4就CUDA out of memory。原因每个worker进程独立加载一份模型、独立申请CUDA context显存占用是线性增加的。这不是Yolov5的问题是PyTorch在多进程下的固有行为CUDA context本身也要占用几百MB显存。解决按显存容量反推worker数量。RTX 306012GB跑Yolov5s建议-w 2RTX 409024GB可以-w 4。另外可以尝试半精度推理——模型加载时加一行model.half()显存占用能降一半左右但要注意CPU模式下不支持half()所以要做设备判断。5.3 请求量一多就卡死响应时间飙到30秒以上现象并发10个请求时部分请求要等20多秒才返回最后直接超时。原因Gunicorn默认的worker类型是同步的每个worker同时只能处理一个请求。如果你的worker数是4第5个请求就得等前面任意一个处理完才能进来而Yolov5单帧推理在GPU上20ms看起来不应该卡——问题往往出在torch.hub.load的force_reloadFalse参数上模型首次加载需要几秒如果多个worker同时首次加载会互相争抢磁盘IO和显存初始化卡顿更明显。解决先把模型预热——服务启动后主动请求一次/detect接口让模型完成加载和CUDA初始化再对外提供服务。Gunicorn的--preload模式下可以在模块顶层加一个预热调用# 在app.py的模块加载末尾执行一次推理预热 if __name__ ! __main__: _ detector.detect(np.zeros((640, 640, 3), dtypenp.uint8))注意这个预热必须放在if __name__ ! __main__里否则本地直接运行python app.py也会额外跑一次推理浪费等待时间。5.4 检测结果坐标和原图对不上现象前端把检测框画出来后框的位置整体偏移或者只画住了目标的一半。原因Yolov5推理时会把输入图片按比例缩放到640x640长边缩到640短边按比例缩放后用灰色填充到640。推理输出的坐标是映射回“原始分辨率”的但如果前端展示时用CSS把图片缩小了而计算坐标时用的是offsetWidth和naturalWidth混用就会偏差。解决前端画框统一以“图片原始分辨率”为基准计算。具体做法是img标签加载完成后记录naturalWidth / naturalHeight画框时用img.clientWidth / img.naturalWidth作为缩放比例不要用videoWidth或别的值。另一个常见错误是把y1、y2做成整数后丢失了精度尤其是小目标坐标偏差几个像素看得非常明显后处理返回坐标时应该保留浮点数前端再取整。5.5 模型能跑但CPU占用100%服务响应特别慢现象没用GPU纯CPU推理每张图耗时800ms以上服务一忙CPU就飙满。原因PyTorch在CPU推理时会自动利用多线程但线程数和实际核心数不匹配时会反复切换上下文更关键的是Flask本身也在抢CPU两者叠加导致推理变慢。解决设置PyTorch的CPU线程数让它和Gunicorn的worker数匹配import torch # 每个worker进程占用2个CPU线程整体不会和Flask抢资源 torch.set_num_threads(2)如果CPU是8核、Gunicorn用-w 4那每个worker占2个线程刚好。注意这个设置要在加载模型之前执行否则模型的算子可能已经按默认线程数完成了编译优化。6. 推理提速技巧与验证方法模型预热、半精度与压测脚本最后一个部分聊验证和提速。模型加载慢是Yolov5部署最典型的体验痛点——一个14MB的Yolov5s模型torch.hub.load首次导入要3到5秒如果预热没做好第一个请求的响应时间会非常难看。我的做法是三个动作叠加模型预热、半精度推理、API压测。模型预热前面已经写了代码这里重点说半精度。def __init__(self, weights_path: str, conf_thres: float 0.25, iou_thres: float 0.45, device: str cpu): self.model torch.hub.load(ultralytics/yolov5, custom, pathweights_path, force_reloadFalse) self.device device if device cuda and torch.cuda.is_available(): self.model.cuda() # 半精度推理显存减半速度提升20%~40%精度几乎无损失 self.model.half() # 注意模型权重和输入都必须转成float16 else: self.model.cpu()半精度有前提条件GPU必须是Volta架构以上Tesla V100、RTX 20系及以上GTX 10系不支持且推理输入图片也要转成float16。由于我封装的是OpenCV的np.ndarray在detect方法里需要加一行转换def detect(self, image): with self._lock: if self.device cuda: # 转成RGB float16否则模型会报输入类型错误 img_tensor torch.from_numpy(image[:, :, ::-1].copy()).float().half() results self.model(img_tensor, size640) else: results self.model(image, size640) # ...做了半精度之后显存占用从约2.3GB降到约1.2GB这意味着Gunicorn可以多跑1个worker整体并发能力跟着提升。但要注意model.half()后如果又调用了model.cpu()权重会变回float32而且模型参数一旦转半精度再转回来精度不会完全恢复所以初始化时就要确定好策略运行中不要切换。最后写一个压测脚本验证服务在并发下的表现。我一般用Python的concurrent.futures写个简单的压测不用上JMeter那种重型工具import concurrent.futures import requests # 读取一张测试图片持续并发20个请求 with open(test.jpg, rb) as f: image_data f.read() def send_request(_): resp requests.post( http://localhost:5000/detect, files{image: (test.jpg, image_data, image/jpeg)} ) return resp.status_code, resp.elapsed.total_seconds() with concurrent.futures.ThreadPoolExecutor(max_workers20) as executor: results list(executor.map(send_request, range(20))) # 统计耗时 latencies [r[1] for r in results] avg_latency sum(latencies) / len(latencies) max_latency max(latencies) error_count sum(1 for r in results if r[0] ! 200) print(f平均耗时: {avg_latency:.3f}s, 最大耗时: {max_latency:.3f}s, 错误数: {error_count})在这个压测里如果平均耗时低于200ms、错误数为0说明服务基本合格如果最大耗时超过5秒优先检查是不是有worker在重复加载模型。注意压测时要连着跑三轮只看一轮的数值没意义——PyTorch首次推理有算子编译预热第二轮开始才是真实性能。我习惯把这三轮数据都记录下来再决定要不要调conf_thres或换更大的模型。这个部署方案是我每次做Yolov5交付都会回头重看一遍的固定套路推理和Web解耦、显存按worker数量核算、预热和压测缺一不可。如果你刚上手建议先跑通第2章的最小接口再加前端页面最后再上Gunicorn和Docker——跳过中间任何一步直接上生产都会在日志里看到意料之外的报错。希望帮到你。本文还有配套的精品资源点击获取
返回列表