简介:面向深度学习入门与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.py | Flask 入口,处理页面路由和图片上传检测 | 表单演示就改这个 |
| restapi.py | 只提供 JSON 接口的独立 API 服务 | 前后端分离时用这个 |
| templates/index.html | Jinja2 模板,页面上传表单和结果展示 | 改交互就改这里 |
| 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 写错」的排查弯路,希望你也能把这套顺序内化成自己的流程。希望帮到你。
本文还有配套的精品资源,点击获取