YOLO Web API设计随笔
YOLO Web API设计随笔
本文Notion链接:Notion
为什么FastAPI?
很显然,作为一群对yolov8尚不了解的大学生。。能够直接使用python进行detect任务对调用是一个极为便利的途径。
或者说我们其实并不清楚怎样用其他的方式使用yolo,从官方的文档中看,我们或许可以将模型转译为tensorflow.js
所需的格式,从而更加从容的使用nodejs作为后端??也许之后会尝试
至于说为什么用fastapi而不是flask,我问了chatgpt,他这么说:
基于 YOLO 模型设计出供 Web 程序调用的 API,你可能会面临处理图像数据、模型推理、API 构建等多个方面的任务。根据这些需求,我会推荐使用
FastAPI。以下是一些原因:
- 异步支持: YOLO 模型的推理通常是相对耗时的任务,而 FastAPI 的异步能力可以在处理多个请求时提供更好的性能和响应时间。
- 类型提示和数据验证: YOLO 模型的输入和输出数据通常有特定的格式,使用 FastAPI
的类型提示和数据验证功能可以确保请求和响应的数据格式正确,并在开发过程中就能发现问题。- 自动文档生成: 你可以使用 FastAPI 自动生成交互式的 API 文档,其中包括请求和响应的数据模型、参数以及示例请求和响应。这对于与其他开发者合作或测试
API 非常有用。- 依赖注入和模块化: FastAPI 的依赖注入系统可以帮助你将不同的功能模块化,比如图像处理、模型推理等,使代码更易于管理和测试。
- 活跃的社区和发展: FastAPI 的社区不断壮大,你可以获得来自其他开发者的支持和贡献,以及许多现成的库和工具,以满足你的需求。
总之,基于 YOLO 模型构建 Web API 需要考虑多个方面,FastAPI 是一个强大且适合现代异步 Web 开发的框架,可以帮助你满足这些需求。
说到底,fastapi的fast真的很吸引我。(起个好名字的重要性)
那就开始吧
首先来看看我希望这个api去做些什么
在上个期末完成了对模型的训练后,已经通过Gradio完成了一个简单的ui实现

那么这个方案有什么问题吗?
有,太慢了!
这种慢并不是模型运行的耗时而是在一个垃圾云服务器上因为带宽限制导致的。没错,gradio给我们的输入和输出方式定义为了图片,也就是我们要把这张图来回上传下载两次,这无疑使愚蠢的。

一种我理解中(我以为)的工作方式
众所周知
webui可是跑在浏览器上的,如果我们把绘制边框的操作交给浏览器(本地)进行,服务器只是回传一段表示位置的数据,那是不是我们就节省了回传一张图片的时间~~
(获得50%的性能提升)~~

另一种我理解中(我以为)的工作方式
从yolo中获取我们所需要的坐标数据
关于坐标数据我们一开始就知道是存在的(废话),也就是save进txt里的那些。但问题是yolov8封装的实在是太好了,他的默认方式居然就是返回图片。好在yolo项目组在假期这几个月里面确确实实有在更新项目,特别是文档,终于变的能看了。现在他们居然写出了整整一页的predict方法解析!!
在**Working with Results
一节中,我们终于看到了result的真实面目:一个对象()**
| Attribute | Type | Description |
|---|---|---|
| orig_img | numpy.ndarray | The original image as a numpy array. |
| orig_shape | tuple | The original image shape in (height, width) format. |
| boxes | Boxes, optional | A Boxes object containing the detection bounding boxes. |
| masks | Masks, optional | A Masks object containing the detection masks. |
| probs | Probs, optional | A Probs object containing probabilities of each class for classification task. |
| keypoints | Keypoints, optional | A Keypoints object containing detected keypoints for each object. |
| speed | dict | A dictionary of preprocess, inference, and postprocess speeds in milliseconds per image. |
| names | dict | A dictionary of class names. |
| path | str | The path to the image file. |
| 属性 | 类型 | 描述 |
|---|---|---|
| orig_img | numpy.ndarray | 原始图像的numpy数组。 |
| orig_shape | tuple | 以(高度,宽度)格式表示的原始图像形状。 |
| boxes | Boxes, 可选 | 包含检测边界框的Boxes对象。 |
| masks | Masks, 可选 | 包含检测掩码的Masks对象。 |
| probs | Probs, 可选 | 包含每个类别的概率的Probs对象,用于分类任务。 |
| keypoints | Keypoints, 可选 | 包含每个对象检测到的关键点的Keypoints对象。 |
| speed | dict | 每张图像的预处理、推理和后处理速度的毫秒字典。 |
| names | dict | 类名的字典。 |
| path | str | 图像文件的路径。 |
注意这个numpy.ndarray 我们可以理解为这个就是图片在python中的多维数组形式
再回来看我们常用的plot命令:
| Method | Return Type | Description |
|---|---|---|
| plot() | numpy.ndarray | Plots the detection results. Returns a numpy array of the annotated image. |
| 方法 | 返回类型 | 描述 |
|---|---|---|
| plot() | numpy.ndarray | 绘制检测结果。返回一个带注释的图像的 numpy 数组。 |
是不是一切都解释的通了
综上,我们只需要从result对象中提取出我们所需要的信息发送给前端,忽略掉冗长的ndarray 就实现我们前面提到的目标了。
还有一个好消息,除了plot,yolo也提供了另一个方法:
| Method | Return Type | Description |
|---|---|---|
| tojson() | None | Convert the object to JSON format. |
| 方法 | 返回类型 | 描述 |
|---|---|---|
| tojson() | 无 | 将对象转换为 JSON 格式。 |
我们来看一下效果
1 | from ultralytics import YOLO |
JSON:
1 | [ |
刚刚好,我们需要的信息都有了,接下来我们要做的就是把这些信息发给我们的浏览器
尝试把JSON信息发送给前端
编写API
我们以最简单的形式入手,当我们访问到某个特定的节点时,我们显示出上面提到的json数据
也就是我们通过浏览器访http://127.0.0.1:8000/detect_results/时,获取到上面提到的json信息。
为了处理这个请求[1],需要在FastAPI应用中创建一个路由[2],使用 @app.get() 装饰器,从而定义一个处理 GET 请求的API端点。
1 | # 修改路由定义,使用 @app.get() |
[3]
这样,当你在浏览器中访问http://127.0.0.1:8000/detect_results/ 时,FastAPI应用将会处理 GET 请求,并将响应的 JSON
信息返回给浏览器。可以在浏览器中看到 JSON 数据。
1 | "[\n {\n \"name\": \"cube\",\n \"class\": 1,\n \"confidence\": 0.7532966136932373,\n \"box\": {\n \"x1\": 85.58421325683594,\n \"y1\": 220.336669921875,\n \"x2\": 379.03240966796875,\n \"y2\": 566.9713134765625\n }\n }\n]" |
对上面的一点解释
-
浏览器访问网址的原理是使用
GET请求来获取网页内容,所以我们需要编写一个处理GET请求的API -
在Web开发中,路由(Route)是一种将URL与特定功能或处理程序相关联的机制。它指定了在访问特定URL时应该执行的代码块或处理逻辑。路由充当了Web应用中不同页面、功能或操作之间的映射关系。每个URL路径都对应一个特定的路由,当访问该URL时,路由会调用与之相关联的处理程序或功能。
-
这段代码包含以下几个步骤
- 步骤 1:导入
FastAPI
1
from fastapi import FastAPI
FastAPI是一个为你的 API 提供了所有功能的 Python 类。- 步骤 2:创建一个
FastAPI「实例」
1
app = FastAPI()
这里的变量
app会是FastAPI类的一个「实例」。这个实例将是创建你所有 API 的主要交互对象。
这个
app同样在如下命令中被uvicorn所引用:1
uvicorn main:app --reload
- 步骤 3:创建一个路径(路由)操作
路径
这里的
「路径」指的是 URL 中从第一个/起的后半部分。所以,在一个这样的 URL 中
https://example.com/items/foo路径会是:/items/foo操作
这里的
「操作」指的是一种 HTTP「方法」。下列之一:POST、GET、PUT、DELETE…以及更少见的几种:
OPTIONS、HEAD、PATCH、TRACE在开发 API 时,你通常使用特定的 HTTP 方法去执行特定的行为。
通常使用:
POST:创建数据。GET:读取数据。PUT:更新数据。DELETE:删除数据。
定义路径操作装饰器
1
@app.get("/")告诉 FastAPI 在它下方的函数负责处理如下访问请求:- 请求路径为
/ - 使用
get操作
@something语法在 Python
中被称为「装饰器」。像一顶漂亮的装饰帽一样,将它放在一个函数的上方(我猜测这个术语的命名就是这么来的)。装饰器接收位于其下方的函数并且用它完成一些工作。在我们的例子中,这个装饰器告诉
FastAPI 位于其下方的函数对应着路径/加上get操作。它是一个「路径操作装饰器」。- 步骤 4:定义路径操作函数
这是一个 Python 函数。每当 FastAPI 接收一个使用
GET方法访问 URL「/」的请求时这个函数会被调用。1
2
3
async def root():
return {"message": "Hello World"}关于是否使用异步函数参考:
- 步骤 5:返回内容
1
2
3
async def root():
return {"message": "Hello World"}💡 待补充
- 步骤 1:导入
引入参数
在上面的例子里,我们将图片的路径写死了(存储在服务器的变量中)这显然是不合理的,毕竟我们不希望我们的API只能识别一张图片。
路径参数
可以使用与 Python 格式化字符串相同的语法来声明路径"参数"或"变量":
1 |
|
不难理解,就是在路径中生命了函数的参数
有类型的路径参数
1 |
|
在这个例子中,item_id 被声明为 int 类型。
设定合法路径值(枚举)
查询参数
声明不属于路径参数的其他函数参数时,它们将被自动解释为"查询字符串"参数
搓一个简单的前端页面
我们直接告诉chatgpt:模仿gradio框架的样式,生成一个图像识别的前端页面,要求包含标题栏,二级标题栏,图像输入上传与展示区,图像结果生成区,置信度调节条,日志输出区
接着,我们稍加整理,就得到了这些
- index.html
1 |
|
- style.css
1 | /* 样式设置 */ |
- main.js
1 | const imageInput = document.getElementById("imageInput"); |
这边采用了canvas来绘制图像的表示框,为了将canvas覆盖到输出图片上,我们在css中声明定位为绝对位置,z轴层级为1(保持在最上层)并且定义了一个更新canvas位置的函数来响应图片尺寸的变化和窗口可能存在的缩放。
现在我们只需要将绘制的坐标更新为实际的位置,并且将触发绘制函数更改为接收到结果时触发即可完成真正的识别展示逻辑。
页面大概长这个样子,使用npm+vite构建,如果你搞到了我这份代码你应该这样运行:
首先来到vite的目录下(长这样):
1 | ---xxx/xxx/vite |
然后打开Terminal,输入 npm i 安装依赖包(前提是你装好了node.js(和npm npm正常情况下随node一同安装))之后会看到出现了一个node_modules文件夹,这就是依赖包
之后可以使用 npm run dev 来开启开发服务器,这个具体的执行命令会参照package.json中"scripts"的部分。正常情况下dev命令只调用 vite,我添加了—host参数来进行局域网内的访问(而不是localhost)。
1 | "scripts": { |
之后就可以在默认端口5173看到部署的网页了

绘制框的效果如下

把图片传给存储服务器
考虑到图像的采集基本上是由单片机(esp32)编码发送,我们暂且使用js在前端模拟(也就是上面提到的上传功能)以mqtt协议发送jepg图片到服务器。
对于MQTT服务器(broker)的选择,这里采用EMQX开源方案作为MQTT代理服务器(MQTT Broker),当然也可以使用MQTT.js自行构建一个server。
- 关于EMQX:
MQTT通信架构
我们先了解一下MQTT,MQTT采用一种发布-订阅的模式进行通信,具体的示意可看下图:

让我们通过对 mqtt.js 的操作来解释这些概念。
首先,在作为发布者(publisher)客户端时,最主要的操作就是发布主题。这可以通过以下方法实现:
1 | publish(topic: string, message: string | Buffer, callback?: DoneCallback): MqttClient; |
从这个方法中,我们可以明确需要提供两个参数。一个是待发布的主题,另一个则是要发布的内容。这个过程就类似于我们在群聊中发送消息,我们需要知道要发送到 哪个群 以及要 发送什么内容。
同时,在消息的接收端,也就是订阅者(subscriber)客户端,主要的操作是订阅主题。这可以通过以下方法完成:
1 | subscribe(topicObject: string | string[] | ISubscriptionMap, callback?: ClientSubscribeCallback): MqttClient; |
随后,您将能够接收到与所订阅主题相关的消息。通过使用.on 方法监听 message 事件,您可以进一步对接收到的负载(消息内容)进行操作(回调函数)。
我们会在后面的内容中详细说明mqtt.js 的具体操作
MQTT Broker搭建
参考官方文档:
既然官方推荐我们使用docker进行部署那就不妨试试,顺便练一下docker怎么用
【GeekHour】30分钟Docker入门教程_哔哩哔哩_bilibili
部署成功后,将端口映射到物理机相同端口,访问 localhost:18083进入默认管理界面
MQTT 客户端(Publisher)编写
我们暂时使用javascript来代替硬件作为发布端
使用库:MQTT.js



