☰
Yolov5目标检测Flask Web部署实战:从模型到API服务
2026/10/7 5:51:35 网站建设 项目流程

简介:面向深度学习入门与Web开发实践者,这份YOLOv5与Flask结合的目标检测Web部署资源,完整演示了从模型加载、图片上传、后端推理到结果返回的整套流程。项目包含可运行的Flask服务端代码、简洁的前端交互页面、Docker部署配置以及接口测试脚本,便于在本地或云服务器快速复现,也为二次开发预留了清晰的扩展点。压缩包共15个文件,整体约187KB,主要含4个Python源码、Markdown说明文档、HTML/CSS页面、Dockerfile以及配置与示例图片等,结构紧凑,适合对照学习。目前已有713人学习下载,可帮助开发者快速打通目标检测模型的Web化落地,是理解模型服务化封装的高性价比入门案例。

1. Yolov5 目标检测的 Flask Web 部署:这份资源到底在解决什么问题?

Yolov5 目标检测模型本身并不难跑通,难的从来不是那行model(img),而是把它变成别人能通过网页上传一张图、等两秒、拿到检测框的可交付物。这份资源做的就是这件事:用 Flask 把 Yolov5 包成一个轻量 Web 服务,既有表单上传页面,也有 REST API。它适合两类人——刚把 Yolov5 训练跑通、却不知道怎么往产品里放的初学者,以及需要一个内部验证工具的工程师。项目代码量不大,结构清清楚楚,花半小时就能复现,后续换上自己的权重文件,就是一套完整的「训练到上线」闭环。

2. 先拆资源:目录结构、推理链路与「先跑通模型再谈 Web」

2.1 资源里每个文件是干什么的

拿到压缩包第一件事不是跑,是先把文件盘明白。这份资源的核心文件不多,我按「入口 → 模板 → 测试 → 部署」四类给你理一遍。

文件作用我的建议
app.pyFlask 入口,处理页面路由和图片上传检测表单演示就改这个
restapi.py只提供 JSON 接口的独立 API 服务前后端分离时用这个
templates/index.htmlJinja2 模板,页面上传表单和结果展示改交互就改这里
static/style.css页面样式不影响功能,可后调
requirements.txt依赖清单必须核对版本,重点排查
Dockerfile容器化部署上线时用,注意系统库
tests_scripts/test_request.py模拟客户端请求 /detect 的冒烟脚本改完代码先跑它
test_inference.py不经过 Web 直接调模型的脚本排查问题首选
README.md资源说明先读它再动手
zidane.jpg自带测试图验证模型是否正常

看到这你大概明白了,app.py 和 restapi.py 功能是重叠的,但路线不同。app.py 走的是「渲染 HTML 表单再回显结果图片」的路线,restapi.py 走的是「只收 POST、只回 JSON」的路线。前者适合演示和内部工具,后者适合给前端页面或小程序做后端。文件列表里的 test_inference.py 是整条链路的摸底工具,我建议你任何改动之前先跑一遍它,确认模型层没崩再碰 Web 层。LICENSE、.gitattributes 这类文件属于仓库配置,运行时用不到,直接忽略。

2.2 torch.hub.load 加载 yolov5 的三种姿势

无论是 app.py 还是 restapi.py,核心加载代码都是同一句 torch.hub.load。这个函数看着简单,但参数之间差别很大,我拆开讲。

import torch # 方式一:从 GitHub 仓库加载官方预训练模型,开箱即用 model = torch.hub.load('ultralytics/yolov5', 'yolov5s', force_reload=False) # 方式二:加载本地权重,适合换成自己训练过的模型,比如鸟类自建数据集 model = torch.hub.load('ultralytics/yolov5', 'custom', path='weights/best.pt', force_reload=False) # 方式三:显式指定本地仓库路径,离线服务器部署必备 # model = torch.hub.load('/home/user/yolov5', 'custom', path='weights/best.pt', source='local')

参数说明:第一行的 'ultralytics/yolov5' 是 GitHub 仓库名,torch.hub.load 会按需下载权重。yolov5s 是 7.3MB 左右的轻量版,往上是 m、l、x 三个版本,精度依次变高、耗时依次变长。第二行的 custom + path 是部署自己模型的标配写法,path 指向你训练出的 .pt 权重。你要做鸟类目标检测,就把 path 换成自己数据集训出来的 best.pt。第三行 source='local' 适合服务器连不上外网的场景,需要先把整个 yolov5 仓库目录传上去。

逻辑说明:我在实际项目里从来不写死方式一。一旦训练了自己的数据集,就得改成 custom + path,否则加载的还是官方 COCO 80 类模型,检测结果跟你的业务目标完全对不上。资源里的 restapi.py 默认用官方权重,是为了开箱即用,你要是上了自己的模型,唯一要改的就是这一行加载代码。

2.3 推理管线:预处理、模型推断和后处理

很多人以为model(img)就结束了,但排错时你必须知道它内部走了哪几步。Yolov5 的推理流程可以拆成三段:第一步 letterbox 预处理,把任意尺寸图像等比缩放到 640×640,空白部分补灰,保证不拉伸变形;第二步进 backbone + neck + head 做卷积推断,输出三组不同尺度的预测;第三步 NMS 非极大值抑制,把重叠的边界框合并,同时过滤掉置信度低于阈值的框。这三步里最容易出问题的是后处理参数。

torch.hub 加载的模型在推断时会带默认配置,conf_thres 是 0.25,iou_thres 是 0.45。但如果你在自定义数据集上目标太小、遮挡太多,默认阈值会让你什么都检测不出来。常见做法是在调用时直接传参:

# 调低置信度阈值,适合小目标或遮挡场景 results = model(img, size=640, conf_thres=0.1, iou_thres=0.5) # 只看置信度高于 0.5 的结果,接口返回更干净 results = model(img, size=640, conf_thres=0.5)

conf_thres 影响召回率,调低会多出框,但也会带来误检;iou_thres 影响重叠框的合并严格程度,目标密集场景要调低。这两个 yolo 超参数玩明白了,比换大模型见效还快。另一个关键参数是 size,640 是速度和精度的平衡点。如果你的机器是 CPU 单核跑,我建议改成 320,速度差不多能快一倍,代价是小目标会漏一些。

2.4 test_inference.py:先跑通模型再碰 Web

我拆这种资源的标准流程是:先不碰 Flask,直接用 test_inference.py 验证权重文件能不能出结果。能出结果,Web 层的问题才不会和模型层混在一起。

import torch from PIL import Image # 加载同一份权重,保证测试环境和 Web 环境一致 model = torch.hub.load('ultralytics/yolov5', 'yolov5s') # 读取自带测试图并推断 img = Image.open('zidane.jpg') results = model(img, size=640) # 打印 pandas DataFrame,这是排查结果最直观的方式 print(results.pandas().xyxy[0]) # 渲染带框图片并保存,肉眼确认比数字更可靠 results.save('runs/detect/')

逻辑说明:results.pandas().xyxy[0] 会输出一张表格,每条记录包含 xmin、ymin、xmax、ymax 四个坐标,以及 confidence、class、name 三列。坐标是 letterbox 处理过但已经映射回原图的,画框可以直接用。results.save 生成带检测框的图片,看这张图比看数字更能确认模型有没有跑偏。资源里自带 zidane.jpg,我一般会再塞两三张自己的图进去,一张有大目标、一张全是小目标、一张什么目标都没有,专门看模型在空图上会不会乱框。

参数说明:xyxy 是左上角和右下角坐标格式,跟 xywh(中心点加宽高)不一样,前端画框时别搞混。test_inference.py 跑通的标准是:图片能保存、终端能打印出至少一条记录。如果这里就报错,后面 Flask 相关的问题都不用查,先把环境和权重解决掉。

3. 把检测服务封装成 Flask 接口:表单上传与 REST API 两条路线

3.1 表单上传:Jinja2 模板 + app.py 的完整链路

app.py 是这套资源的主入口,职责是「渲染页面 + 处理上传 + 回显结果」。整条链路是:浏览器 GET / 返回 index.html 表单,用户选图点提交,POST /detect,后端解析图片、调模型、把带框图回传。下面这个版本是 app.py 里最常见的写法:

from flask import Flask, render_template, request, send_file import torch from PIL import Image import io app = Flask(__name__) # 模型全局加载一次,别放在路由里每次请求都 load model = torch.hub.load('ultralytics/yolov5', 'yolov5s') @app.route('/', methods=['GET']) def index(): return render_template('index.html') @app.route('/detect', methods=['POST']) def detect(): file = request.files.get('image') if not file: return 'no image', 400 # 文件流直接转 PIL 图片,不用先存盘 img = Image.open(file.stream) results = model(img, size=640) # 画框并转成 PNG 返回给前端 rendered = results.render()[0] img_out = Image.fromarray(rendered) buf = io.BytesIO() img_out.save(buf, format='PNG') buf.seek(0) return send_file(buf, mimetype='image/png') if __name__ == '__main__': app.run(host='0.0.0.0', port=8000)

逻辑说明:核心是 request.files.get('image'),它拿到的是上传的文件对象,file.stream 是文件流,PIL 的 Image.open 可以直接读,省了一步临时文件落盘。results.render() 返回带框的 RGB 数组,转成 PNG 直接回给前端,浏览器就能看到标注结果。这里不走 JSON 是因为表单页直接显示图片更直观,适合内部工具和演示环境。

参数说明:app.run 的 host 改成 0.0.0.0,是为了让同一局域网的其他机器也能访问,默认 127.0.0.1 只有本机能打开。port=8000 是监听端口,如果 80 被占用,就换 8080、8001 这类端口,但要记得调用方和防火墙同步改。模板里 index.html 的 form 必须设置 enctype="multipart/form-data",否则 Flask 拿不到文件,这个坑后面会专门讲。

3.2 REST API:restapi.py 与统一 JSON 返回

表单上传适合人用,接口更适合程序用。restapi.py 的思路是把检测结果序列化成 JSON,交给前端、小程序或者另一个服务去消费。它跟 app.py 的差别只在于路由和返回方式:

from flask import Flask, request, jsonify import torch from PIL import Image app = Flask(__name__) model = torch.hub.load('ultralytics/yolov5', 'yolov5s') @app.route('/detect', methods=['POST']) def detect(): image = request.files.get('image') if image is None: return jsonify({'error': 'missing image'}), 400 img = Image.open(image.stream) results = model(img, size=640) # 关键:把 DataFrame 转成 JSON 可序列化的结构 detections = results.pandas().xyxy[0].to_dict(orient='records') return jsonify({ 'success': True, 'count': len(detections), 'detections': detections }) if __name__ == '__main__': app.run(host='0.0.0.0', port=8000)

逻辑说明:to_dict(orient='records') 是这个接口的灵魂。结果 DataFrame 直接返回会报 TypeError,因为 Flask 的 jsonify 不认识 pandas 对象,转成 record 列表后,每条记录就是一个独立字典,前端通过下标就能读。我通常会额外加一个 count 字段,方便前端在拿到空数组时快速判断这次调用有没有检出目标。

参数说明:接口只接受 POST,因为 GET 传图片要么用 base64 要么塞 query 参数,都不适合大文件。缺图片时返回 400,这是 REST 接口的基本素养。真正常被人忽略的是超时设置——Yolov5s 在 CPU 上跑一张图可能要 1 到 3 秒,nginx 的 proxy_read_timeout 默认 60 秒没问题,但如果是短超时的内部网关,就要调大,否则前端会先于模型报错。

3.3 前端页面:index.html 到底该怎么写

资源里的 templates/index.html 承担了「选图 → 提交 → 回显」三个交互。Jinja2 模板没有魔法,就是一个带表单的 HTML,重点在 form 标签的属性:

<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Yolov5 目标检测</title> </head> <body> <h2>上传图片进行目标检测</h2> <form action="/detect" method="post" enctype="multipart/form-data"> <input type="file" name="image" accept="image/*" required> <input type="submit" value="开始检测"> </form> </body> </html>

逻辑说明:form 的 action 指向后端路由 /detect,method 必须是 post,enctype 必须是 multipart/form-data,这三样少了任何一个,Flask 的 request.files 都拿不到数据。input 的 name="image" 必须和后端 request.files.get('image') 保持一致,这个对应关系写错是最常见的 400 或 None 报错来源。

如果走 REST API 路线,前端不用提交表单,而是用 fetch 上传:

const formData = new FormData(); formData.append('image', fileInput.files[0]); fetch('/detect', { method: 'POST', body: formData }) .then(res => res.json()) .then(data => { console.log(data.count, data.detections); });

参数说明:FormData.append 的 key 同样要叫 'image',和后端对应。前端拿到 JSON 后,坐标直接用 detections 里的 xmin/ymin/xmax/ymax,记得这些值都是浮点,画框时要么取整,要么乘上图片实际显示比例,不然框会偏移。static/style.css 只是控制页面布局,不影响接口逻辑,可以最后再调。

3.4 requirements.txt:版本就是「后悔药」

项目里 requirements.txt 是我每次必看也必改的文件。它决定了你在哪个依赖版本上跑,而这一项能直接决定你在坑里待多久。Yolov5 官方对 torch 和 torchvision 的版本绑定非常敏感,torch 2.x 和 torch 1.13 上的推理结果会有细微差异,opencv-python 版本不对还可能和系统库冲突。

我一般会在 requirements.txt 里锁成这一组:

flask==2.2.5 torch==1.13.1 torchvision==0.14.1 opencv-python==4.6.0.66 pandas==1.5.3 numpy==1.23.5 pillow==9.4.0

参数说明:torch 和 torchvision 的版本必须配套,torch 1.13.1 对应 torchvision 0.14.1,这个映射关系在 PyTorch 官网有对照表,写错会直接报 torchvision 里某个 C 库找不到。numpy 锁 1.23.5 是为了兼容 Yolov5 的推理脚本,新版 numpy 2.x 在部分旧代码里会报 numpy.bool 不存在。如果你用的是 Python 3.10 以下,这个组合基本不会翻车。

提示:装完依赖先跑一遍torch.cuda.is_available(),确认环境可用再继续 Flask,避免后面所有错误都指向同一处。

逻辑说明:我第一次图省事,用 pip install ultralytics 把所有依赖装到最新版,结果 torch.hub 和 torchvision 算子冲突,推理时直接崩掉。那之后我养成习惯:先建虚拟环境,按 requirements.txt 安装,跑 test_inference.py 验证,能跑通再动 Flask。版本问题就是后悔药,锁对了能少吃很多苦。

4. 部署避坑:五个最常见的翻车现场

4.1 torch.hub.load 卡在 Downloading,启动服务等半天

现象:执行 python app.py 后,控制台停在一段中英文混合的 Downloading 日志上,几分钟都起不来,网络差的时候还会直接报连接超时。

原因:torch.hub.load 第一次调用会去 GitHub 拉取仓库代码和权重文件,网络环境不好时连接会一直挂着。即使你之前下载过,如果 force_reload 被误设成 True,也会强制重下。这种情况在离线服务器上尤为常见,模型没下载全,整个 Web 服务就瘫在启动阶段。

解决:把权重文件提前准备好,换成 local 方式加载。比如把 yolov5s.pt 下载好放在 weights 目录,再用model = torch.hub.load('ultralytics/yolov5', 'custom', path='weights/yolov5s.pt', force_reload=False)。如果服务器完全连不上外网,就指定source='local',把整个 yolov5 仓库也传上去。这样模型加载不依赖外网,启动时间从几分钟降到几秒。我一般会在项目里建一个 weights 目录,把常用权重和对应说明文件一起归档,换机器时直接拷贝,不再浪费时间重新拉取。

4.2 上传中文名图片,检测结果为空或者直接报错

现象:上传 zidane.jpg 一切正常,可用户传一张「无人机航拍01.jpg」,接口返回 detections 是空数组,有时甚至报 UnicodeDecodeError。

原因:Flask 的 request.files 拿到的文件名带中文,PIL 在部分实现里处理中文路径会读取失败,或者图片带了 EXIF 旋转信息,PIL 默认不处理,导致检测框和实际画面方向不一致,看起来就像没检测到目标。

解决:在后端入口处统一改名,接收文件后丢弃原文件名:

import uuid from PIL import Image # 用 uuid 重命名,彻底绕开中文文件名的解码问题 filename = str(uuid.uuid4()) + '.jpg' img = Image.open(file.stream)

逻辑说明:uuid 方案一劳永逸,既符合安全规范,避免路径穿越之类的文件名注入风险,又绕开中文解码问题。EXIF 旋转的坑是另一个层面,竖拍照片的 EXIF 里记录了旋转方向,但 Image.open 不会自动应用,需要先用 ImageOps.exif_transpose 把图片转正再送模型,否则检测框会整体偏转 90 度。

4.3 并发请求一多,GPU 显存被打爆

现象:本地单张测试没问题,部署到服务器后,前端一刷新就报 CUDA out of memory,或者推理越来越慢,请求排队几十秒。

原因:模型虽然全局只加载一版,但每个请求进来都会跑一次前向推理,如果代码里每请求都做设备搬移,显存碎片会被频繁分配释放。更常见的是多人同时上传时,推理队列积压,显存放不下连续批次的中间特征图。

解决:两条路,一是控制并发,二是固定显存分配。控制并发最简单的方式是用线程锁把推理部分串行化:

import threading infer_lock = threading.Lock() @app.route('/detect', methods=['POST']) def detect(): # 加锁让推理串行,防止多请求同时吃显存 with infer_lock: results = model(img, size=640)

参数说明:加锁的代价是请求会排队,但对目标检测这种 CPU/GPU 密集操作,排队比崩溃强得多。想要更高吞吐,把输入 size 从 640 降到 320,显存占用明显下降。再往上就是模型量化,把权重转成 FP16 推理,显存占用直接减半,精度损失在小模型上基本看不出。这些都属于工程取舍,先保稳定再谈速度。

4.4 前端画框偏移,框的位置和物体对不上

现象:JSON 返回的坐标看起来正常,前端用 canvas 画框,但框整体偏左上角,或者框比目标小一圈。

原因:两个最常见的来源。第一,Yolov5 返回的坐标是相对原图的,但前端显示图片时用了 CSS 缩放,你没有按缩放比例换算坐标。第二,如果你用的是 results.render() 输出的图,却忘了 Yolov5 已经在渲染时还原坐标,再套一层换算就会重复偏移。

解决:在前端统一按图片实际显示尺寸换算:

// imgWidth 是图片原始宽度,800 是页面实际显示宽度 const scale = 800 / imgWidth; ctx.strokeRect(xmin * scale, ymin * scale, (xmax - xmin) * scale, (ymax - ymin) * scale);

逻辑说明:先拿图片原始尺寸和页面渲染尺寸算出 scale,所有坐标都乘以这个比例。用 results.render() 输出图时,坐标已经还原过,只需要保证渲染图宽度和画布宽度一致。我排查这种问题时的笨办法是:先固定用原图 1:1 显示跑通画框,再引入缩放,一旦框歪了,问题只能出在 scale 换算或坐标还原这两处。

4.5 Docker 容器里 PIL 和 OpenCV 缺依赖,一启动就报错

现象:本地跑得好好的,docker build 完成后 docker run,日志报 libGL.so.1 找不到,或者 PIL 的 _imaging 模块加载失败,服务起不来。

原因:python:3.9-slim 这类精简镜像没有安装图形库,PIL 和 OpenCV 的底层依赖 libgl1、libglib2.0-0 不在镜像里。requirements.txt 只装了 Python 包,系统库必须单独装。

解决:Dockerfile 里在装依赖前先补系统库:

FROM python:3.9-slim RUN apt-get update && apt-get install -y libgl1 libglib2.0-0 WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD ["python", "restapi.py"]

逻辑说明:libgl1 是 OpenCV 图像读写需要的底层库,libglib2.0-0 是 PIL 二进制扩展的公共依赖,缺哪个都会在 import 阶段直接炸。这两行是 Docker 化部署最常见的补丁。注意 apt-get 装完要清理 /var/lib/apt/lists 缓存,不然镜像体积会大不少,这是我在镜像瘦身时踩出来的经验。

5. 上线前再做三件事:Docker 化、GPU 加速与接口冒烟验证

5.1 构建镜像并跑起来

把 restapi.py 作为 CMD 入口,构建并启动:

docker build -t yolov5-flask . docker run -d -p 8000:8000 --name yolo-web yolov5-flask

参数说明:-d 后台运行,-p 8000:8000 把容器 8000 端口映射到宿主机,--name 方便后续用 docker logs 查日志。如果服务器有 GPU,加 --gpus all 就能让容器里 PyTorch 用上 GPU,但前提是镜像里装的是 CUDA 版 torch。

5.2 GPU 加速与量化

确认 PyTorch 真的用了 GPU,不要只看 nvidia-smi,要在代码里验证:

import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))

is_available() 返回 False,最常见原因是 PyTorch 装成了 CPU 版,需要重新安装带 CUDA 的版本。确认之后可以开 FP16 推理:

results = model(img, size=640, half=True)

half=True 让权重和输入转为 FP16,推理速度提升明显,显存占用减半。量化是更深的优化,Yolov5 官方支持 INT8 导出,需要校准数据集。你要是想把模型跑到 RK3588 这类边缘板子上,导出 ONNX 加 INT8 是必经之路,但那是另一个大工程,先把 half 用起来性价比最高。

提示:CPU 环境下 half=True 没有加速效果,反而可能因算子缺失报错,只有 CUDA 设备上才推荐开启。

5.3 用 test_request.py 做接口冒烟验证

这是上线前我最依赖的一步。用 requests 模拟真实调用:

import requests url = 'http://127.0.0.1:8000/detect' files = {'image': open('zidane.jpg', 'rb')} resp = requests.post(url, files=files) print(resp.status_code) print(resp.json())

逻辑说明:跑通后要断言三件事:状态码是 200,返回体里 detections 不是空,count 和 detections 的长度一致。如果前两步过了但第三步不一致,多半是 JSON 序列化时丢了记录,回头查 to_dict 的用法。这个脚本要留好,之后每次改模型或改接口,先跑它,能省掉大量反复刷网页的时间。

从那以后,我每次改这个项目都会强制走一遍固定顺序:先跑 test_inference.py 验证模型层,再跑 test_request.py 验证接口层,最后才允许自己动前端。这个习惯帮我挡掉了不知道多少「模型没跑通却以为是 Web 写错」的排查弯路,希望你也能把这套顺序内化成自己的流程。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询