简介:这是一份结合 Yolov5 与 Flask 的实时目标检测网页应用项目,面向具备一定深度学习基础、希望把模型快速部署到网页端提供检测服务的技术人员。它解决从模型加载、图片上传到检测结果返回的完整流程,适合用于算法演示或业务验证。压缩包共 15 个文件,大小仅 187KB,包含 4 个 Python 脚本,分别承担 Flask 主程序、接口服务、推理与测试;同时提供容器化部署配置、前端页面与样式、依赖清单及说明文档,结构紧凑、上手成本低。已有 713 人学习下载。内容覆盖预训练模型加载、检测接口设计、图像预处理与结果格式化,并附带测试脚本和部署思路;开发者可在其基础上扩展自定义检测场景,也可进一步优化推理速度与模型体积,适合作为课程设计或项目实战参考。
1. 把YOLOv5塞进Flask:从命令行到网页上传一张图的距离
很多人训练完YOLOv5,最不想面对的就是部署。notebook里python detect.py --source xx.jpg跑得飞起,可一说到要给别人用、要接到业务系统上,就没了头绪。这套项目把 YOLOv5 目标检测和 Flask 框架捏在一起,给出一条完整的 Web 部署路径:浏览器传图、后端推理、结果回显,页面和接口两种方式都齐。它适合手里有训练好的权重、想把检测能力开放成HTTP接口的开发者,也适合第一次做模型部署、想找一份能照着改的参考的你。
2. 项目结构与推理链路:restapi.py 和 app.py 到底谁在干活
2.1 先读懂这套文件的职责划分
解压项目后你会看到一堆文件,先别急着跑,花两分钟把职责捋清楚,后面改代码就不会到处乱翻。核心是app.py和restapi.py:前者管Web页面和路由,后者是检测逻辑的API封装。templates/index.html和static/style.css是前端部分,requirements.txt管依赖,Dockerfile管容器化,test_request.py是接口自测脚本,zidane.jpg和pytorch.png是现成的测试图,readme_copy.md可以当备查阅的说明副本。
| 文件 | 职责 | 备注 |
|---|---|---|
| app.py | Flask主入口,页面路由与上传处理 | python app.py启动 |
| restapi.py | 检测API封装,返回JSON | 可独立启动或由app引用 |
| templates/index.html | 上传表单与结果展示 | Jinja2模板 |
| static/style.css | 页面样式 | Flask默认静态目录 |
| requirements.txt | Python依赖清单 | torch/flask/pillow |
| test_request.py | 模拟客户端POST图片 | 改URL即可复用 |
| Dockerfile | 容器构建配置 | 注意镜像体积(见避坑) |
| LICENSE | 开源许可文件 | 商用前建议读一遍 |
这里有个容易看走眼的地方:app.py和restapi.py不是二选一的关系,而是两种使用方式。我见过不少人把 restapi.py 里的代码直接复制进 app.py,结果路由冲突。合理的设计是让app.py按需引用restapi.py里的检测函数,或者干脆用 Flask 的 blueprint 把API模块挂进来。检测逻辑抽成独立函数还有个好处——以后换 FastAPI 或加异步队列,模型代码一行不用动。
2.2 推理链路的核心:模型加载与结果解析
整个服务的命脉就两段:模型怎么加载、检测结果怎么变成能扔给前端的JSON。先看加载这一段。
import torch model = torch.hub.load('ultralytics/yolov5', 'yolov5s', force_reload=False)torch.hub.load会从GitHub拉取 ultralytics/yolov5 仓库代码,再加载yolov5s权重。yolov5s 是small版本,体积小、速度快、精度够用;如果你有自己的训练权重,把第二个参数换成你best.pt的路径即可。force_reload=False这个参数很关键——它决定每次启动要不要重新下载仓库和权重。设成False,第二次启动直接走本地缓存,离线也能起来;设成True,每次启动都会去检查远程更新,在有防火墙或网络受限的机器上,这是启动卡死的第一大原因。
接着看检测与解析。摘要里给了一个简化版,实际项目里process_results是少不了的,不然results对象直接塞给jsonify一定会报类型错误。补全后的完整逻辑是这样:
from flask import Flask, request, jsonify import torch import io from PIL import Image app = Flask(__name__) model = torch.hub.load('ultralytics/yolov5', 'yolov5s', force_reload=False) @app.route('/detect', methods=['POST']) def detect(): file = request.files.get('image') if file is None: return jsonify({'error': 'no image field'}), 400 img = Image.open(io.BytesIO(file.read())) results = model(img, size=640) df = results.pandas().xyxy[0] detections = [] for _, row in df.iterrows(): detections.append({ 'class': row['name'], 'confidence': round(float(row['confidence']), 4), 'bbox': [int(row['xmin']), int(row['ymin']), int(row['xmax']), int(row['ymax'])] }) return jsonify({'detections': detections})逐段说。request.files.get('image')取的是表单里name为image的文件字段,前端input的name必须和它对上。Image.open(io.BytesIO(file.read()))是关键一步:PIL可以直接从内存字节流开图,避免先把文件存到磁盘再读——Web场景下每多一次磁盘IO都是在拖慢响应。results.pandas().xyxy[0]把YOLO的输出转成pandas表格,行列对应的东西有:
xmin/ymin/xmax/ymax:边界框坐标,像素单位confidence:置信度,0到1class/name:类别索引和类别名
新手最容易在这翻车:results.xyxy[0]返回的是Tensor,包含归一化坐标,直接拿来算像素框会偏;pandas()返回的是原始像素坐标,配合int()转成整型,正好是前端画框要的格式。
权重选择上再补一句。yolov5s 精度够、速度最快;要更高精度可以换 yolov5m 或 yolov5l,代价是推理时间和内存上升。如果你用自己的数据集训练的 best.pt,加载方式不变,但要注意训练时的输入尺寸——训练用了 1280×1280,推理时设 640 会丢精度;反过来推理设成 1280 会显著变慢。这些参数之间的取舍没有标准答案,只有场景答案。
2.3 前端怎么跟后端对话:模板、上传与结果绑定
templates/index.html做的事很朴素:一个表单带上传控件,提交后把图片通过POST发给后端,再把检测结果渲染回页面。Jinja2模板的核心写法如下:
<form method="post" action="/detect" enctype="multipart/form-data"> <input type="file" name="image" accept="image/*" required> <button type="submit">开始检测</button> </form> {% if detections %} <ul> {% for d in detections %} <li>{{ d['class'] }}:置信度 {{ d['confidence'] }}</li> {% endfor %} </ul> {% endif %}enctype="multipart/form-data"是上传表单的标配,少了它Flask端拿不到文件流;accept="image/*"只是浏览器侧的筛选提示,后端不能省校验。action="/detect"指向后端的POST路由,注意如果用app.py启动且页面路由和API路由不同,form里这个action要跟着改。服务端渲染时,路由里得把结果传进模板:
from flask import render_template @app.route('/', methods=['GET', 'POST']) def index(): detections = None if request.method == 'POST': # 同样的推理逻辑,结果赋值给 detections pass return render_template('index.html', detections=detections)静态资源方面,static/style.css要放到static/目录下,HTML里用url_for('static', filename='style.css')引用。Flask对静态文件目录有默认约定,别改乱,否则样式404报得莫名其妙。
3. 从0到1跑通全流程:环境、启动与HTTP验证三板斧
3.1 环境准备:Python版本、PyTorch与依赖安装
先把运行环境装明白。这套项目依赖的核心是 torch、flask、pillow,requirements.txt里写着依赖清单。装之前确认一下Python版本:PyTorch对 3.8-3.10 支持最稳,3.11 或更高版本建议先查一下是否有对应wheel,装不上整个应用都起不来。
python -m venv venv source venv/bin/activate # Windows是 venv\Scripts\activate pip install -r requirements.txt这里有个细节值得注意:torch.hub.load('ultralytics/yolov5')会自动拉取yolov5仓库的代码,所以requirements.txt里通常不用显式列yolov5包。但这也意味着首次运行必须有外网且能访问GitHub。如果你在离线或受限网络环境,正确做法是先把 ultralytics/yolov5 仓库 clone 到本地,改用source='local'加载,详见避坑章节。
Conda用户也可以用conda create -n yolov5 python=3.9起环境,再走pip安装。环境装好后,先用一条命令确认PyTorch和CUDA的匹配:
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"输出里torch.cuda.is_available()为True说明GPU可用,为False就老老实实用CPU跑。CPU推理yolov5s一张640图大约2-5秒,演示够用;要上生产且追求实时,务必上GPU。
提示:生产环境装完依赖后看一眼
pip show torch,确认装的是CPU版还是CUDA版。带+cu后缀的是CUDA版,没GPU的机器装它纯属浪费体积和内存。
3.2 启动服务:两种启动方式的适用场景
项目提供两种启动入口。python app.py启动带完整页面的应用,适合给人演示或自己用浏览器点一点;python restapi.py启动纯接口模式,只返回JSON,适合给别的系统调用。两者端口默认都是8000,注意别同时起。
python app.py # 页面模式,访问 http://localhost:8000 python restapi.py # API模式,直接POST http://localhost:8000/detect启动后看到Running on http://0.0.0.0:8000说明Flask起来了。host='0.0.0.0'表示监听所有网卡地址,容器里和局域网内都能访问;如果只需要本机调试,改成127.0.0.1。启动前先在项目目录下确认没有旧进程占用端口,反复改代码的经历里很常见:Ctrl+C没杀干净,后台还挂着一个旧实例,新实例起不来,你还在怀疑自己代码写错了。
调试阶段可以debug=True,改代码自动重载,报错带完整堆栈。但一旦服务要被外部访问,必须关掉。Flask的debug模式自带远程控制台,暴露出去等于把服务器钥匙交给别人,这不是危言耸听。restapi.py 纯接口模式还有一个设计取舍:跨域调用时要在响应里加CORS头,Flask-CORS一行代码能解决,忘了加前端浏览器会直接拦掉响应,报No 'Access-Control-Allow-Origin' header。
3.3 用 test_request.py 验证接口:从自测脚本到确认链路
启动服务后,项目里的test_request.py就是你的第一个客户端。它模拟外部调用方,把zidane.jpg传给服务端检测,打印返回状态码和JSON。核心写法如下:
import requests url = 'http://localhost:8000/detect' with open('zidane.jpg', 'rb') as f: resp = requests.post(url, files={'image': f}) print('HTTP状态码:', resp.status_code) data = resp.json() for d in data.get('detections', []): print(f"{d['class']}: {d['confidence']}, bbox={d['bbox']}")重点看files={'image': f}的键名image。Flask端用request.files.get('image')取文件,键名必须两边一致;改了表单的name就要同步改这里,否则后端收到空值,直接返回400。resp.json()把响应解析成字典,前端页面拿到同样结构就能渲染。
如果接口返回的不是JSON而是HTML,多半是路由报错被Flask当异常页面处理了。把服务端控制台的堆栈贴出来看,最常见的是PIL打不开非图片文件、或模型推理抛异常,这两类往下走避坑部分都有对应解法。
验证通过的标准是:状态码200,detections数组非空,包含至少一个类别名。到这一步,整条「上传→推理→JSON返回」的链路就算闭环。测试脚本还建议补几个边界场景:不带文件直接POST、传个文本文件、传超大图。你的联调对象不一定会按规矩来,接口先把这些情况拦住,省得后面扯皮。
4. 避坑指南:部署YOLOv5+Flask我踩过的五个坑
4.1 启动卡在Downloading:网络受限下的模型加载
现象:执行python app.py后,控制台停在Downloading...很久不动,最后报Connection timed out或ReadTimeoutError;本地明明跑通过的代码,换个网络环境就起不来。
原因:torch.hub.load('ultralytics/yolov5', 'yolov5s')首次运行要从GitHub拉取yolov5仓库代码和权重。生产服务器一般在内网,或没放行到GitHub的HTTPS。默认force_reload=False只对本地已有缓存生效,缓存不存在时照样去访问网络。
解决:提前把仓库和权重准备好,改用本地加载:
git clone https://github.com/ultralytics/yolov5.git /opt/yolov5model = torch.hub.load('ultralytics/yolov5', 'yolov5s', source='local', path='/opt/yolov5', force_reload=False)source='local'让torch.hub完全不联网,直接从指定路径读取仓库代码;权重放在仓库的weights/目录下也能识别。我习惯把整套yolov5目录打进镜像或同步到服务器,之后每次启动都走本地,既快又稳。注意path指向的必须是包含hubconf.py的仓库根目录,不是weights子目录,写错了会报找不到模型定义。
4.2 并发一上来就卡死:Flask开发服务器的单线程宿命
现象:自己点着玩一切正常,几个同事同时上传图片,服务瞬间失去响应;吞吐量一上来,请求排队越来越长,最后直接超时崩溃。
原因:Flask自带的app.run()启动的是Werkzeug开发服务器,单进程单线程。代码里的推理是同步阻塞的,一个请求卡在model(img)上时,后面所有请求都在排队。图片大、CPU推理慢,雪崩来得更快。
解决:低并发场景先用threaded=True撑一下:
app.run(host='0.0.0.0', port=8000, threaded=True)并发真上来了,换gunicorn多worker才是正规解法:
gunicorn -w 4 -b 0.0.0.0:8000 app:app-w 4表示4个worker进程,每个进程独立加载一份模型权重,等于四路推理并行。代价是内存线性增长——yolov5s权重不大,但PyTorch运行时每个worker要吃几百MB内存,2GB内存的机器跑4个worker会吃力,按实际资源减到2个。另外gunicorn只支持Linux,Windows用户要么换WSL,要么先忍着头用threaded模式。
4.3 上传图片报400或413:请求体限制与格式陷阱
现象:小图测试通过,换手机拍的照片直接返回413或400;后端request.files.get('image')取到None,请求都拦在最前面。
原因:常见两个来源。一是部署链路里有限制请求体大小的配置,比如Nginx默认client_max_body_size是1MB,Flask端如果显式配了MAX_CONTENT_LENGTH也会拦。二是前端表单漏了enctype="multipart/form-data",浏览器把文件当普通表单字段提交,后端拿到的不是文件流。
解决:Flask端显式调大限制,同时前端表单补上enctype:
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB<form method="post" action="/detect" enctype="multipart/form-data">再给检测接口加一道解码保护,因为有些"图片"其实是损坏文件或改后缀的文本,PIL解不开会直接抛异常把路由带崩:
try: img = Image.open(io.BytesIO(file.read())) img = img.convert('RGB') except Exception: return jsonify({'error': 'invalid image'}), 400convert('RGB')顺手处理了带透明通道的PNG——YOLO模型输入要求三通道,直接推理四通道图在某些torch版本下会报维度错误,这种错最隐蔽,报错信息跟图片格式八竿子打不着。
4.4 Docker镜像体积爆炸:一个torch毁掉整个镜像
现象:按常规流程写好Dockerfile,docker build完一看镜像快2GB,推到仓库慢、拉下来更慢。
原因:pip install torch默认装的是带CUDA的完整版,几个G的依赖全进镜像了。GPU机器上用没问题,但如果构建的是CPU推理镜像,全是冤枉体积。还有一部分体积来自.dockerignore没配好,本地的venv、.git、缓存全被复制进镜像。
解决:CPU场景下显式指定CPU版torch,并写好.dockerignore:
pip install torch --index-url https://download.pytorch.org/whl/cpu__pycache__/ *.pyc venv/ .git/ models/ *.pt_注意:没有GPU的服务器千万别装CUDA版torch,镜像体积是一个教训,推理速度还一点没提升。
Dockerfile里用多阶段构建,把依赖层单独缓存,也能显著降低重复构建成本。别小看这一步,镜像从2GB压到1GB以内,部署一次省不少时间。
4.5 端口被占用:8000不是你想用就能用
现象:Address already in use,Flask起不来;或者之前的进程还占着窗口,新开终端再跑总是连到旧实例上,改了代码却看不到效果。
原因:上次Ctrl+C没杀干净进程,或者同机其他服务占用了8000端口。Flask只在启动时绑定端口,几遍启动点下来,残留进程全挂在8000上。
解决:先查再杀:
lsof -i :8000 # 看谁占了 kill -9 <PID> # 按PID杀掉项目里app.run(port=8000)是写死的,多个服务要共存时改成port=8001,或者用环境变量os.getenv('PORT', 8000)传入。我有一次线上排查半天,最后发现是CI脚本同时拖了两个实例抢同一个端口。从那以后,固定端口一律写成配置项,不再硬编码在代码里。
5. 进阶:把demo打磨成值得长期用的检测服务
能跑和能用之间还隔着几步。我每次把这种模型服务交出去之前,都会强制做三件事。
5.1 模型预热:别让第一个请求等十几秒
Flask启动后第一个请求会慢到离谱,因为PyTorch在第一次推理时才做CUDA初始化、算子编译这类惰性操作。解决方法是启动前先用一张占位图跑一遍:
with torch.no_grad(): model(torch.zeros(1, 3, 640, 640))这段放在app.run之前,服务开始监听端口时模型已经是"热"的。用户看到的第一个请求响应时间从十几秒掉到正常水平。torch.zeros(1, 3, 640, 640)是一个全零的占位张量,形状和真实输入一致,推理一次把该初始化的都初始化完,然后扔进model内部被复用。注意包在torch.no_grad()里,避免预热过程被记入计算图,白占内存。
5.2 置信度过滤:接口层做好第一道筛选
默认置信度阈值0.25会把很多低质量框一起返回。接口层加一个可选的conf参数:
results = model(img, size=640) df = results.pandas().xyxy[0] df = df[df['confidence'] >= request.args.get('conf', 0.4, type=float)]request.args.get('conf', 0.4, type=float)的意思是:从URL查询参数里读?conf=0.5这样的值,没传就用0.4。调用方对精度和召回率的要求不一样,把这个决定权留给业务侧,比自己闷头调阈值灵活得多。前端展示时同样过滤一次,不然满屏乱框看着像模型失效。
5.3 结构化错误与超时兜底
所有异常统一返回JSON而不是HTML错误页:
@app.errorhandler(Exception) def handle_error(e): return jsonify({'error': str(e), 'detections': []}), 500调用方至少能稳定拿到一个JSON结构去解析,而不是面对一堆堆叠信息。再做一层超时保护,防止模型偶发卡死把整个请求挂住,gunicorn配置里加--timeout 30就能让worker在30秒后主动放弃。
这三步做完,这个服务才勉强算得上"可交付"。以后再部署这类模型服务,我都会强制走一遍预热、置信度过滤、结构化错误,别嫌麻烦——被线上调接口的人追着骂过就懂了。希望帮到你。
本文还有配套的精品资源,点击获取