☰
YOLOv11模型部署实战:OpenCV DNN推理与FastAPI接口封装
2026/10/21 4:01:36 网站建设 项目流程

“训练完模型只是第一步,真正让YOLOv11发挥价值的是把它部署成别人能调的接口。”这句话我每次遇到项目上线都得跟新同学说一遍。第008篇,咱们直接进入模型部署实战,围绕YOLOv11目标检测模型,用Python的OpenCV做图像预处理和DNN推理,再用FastAPI把整个推理过程封装成HTTP服务。这篇笔记会从方案选型、模型转换、接口编写、性能优化到常见故障排查,按一条完整链路走下来,适合已经把YOLOv11跑通训练、正准备把模型接到业务系统里的开发者。

我不打算只贴一段能跑的代码,然后说“你复制就能用”。部署这层水很深:同样的模型,接口写得好不好,响应速度能差好几倍;并发一上来,处理不好直接雪崩;模型格式选不对,线上设备根本跑不起来。这一篇会把我在实际项目里踩过的坑、验证过的调优参数、以及排查问题的套路都记录下来,你照着走能省掉至少一星期的试错时间。

1. 部署方案怎么选:为什么是OpenCV+FastAPI,而不是别的

1.1 三种常见部署路径对比

YOLOv11训练完拿到的是PyTorch权重文件,但这玩意儿不能直接丢到生产环境。原因很简单:线上服务不可能为了跑一个检测模型就把PyTorch全家桶装上,更重要的是PyTorch动态图的推理性能并不理想,并发场景下资源开销非常大。我在项目里一般会对比三条路线。

第一条是用TorchServe或者自己写一个PyTorch的加载脚本,直接把.pt文件加载到GPU上推理。优点是省事,训练时怎么调用模型,部署时基本也怎么调用。缺点是每次请求都要经过Python的GIL和PyTorch的算子分发,延迟抖动厉害;而且.pt文件依赖Python版本和Torch版本,升级环境容易出兼容问题。

第二条是把模型转成ONNX,再用ONNX Runtime加载推理。ONNX Runtime的CPU推理速度通常比PyTorch快不少,模型文件也更小,部署环境干净。但ONNX Runtime对预处理这块支持比较弱,图像缩放、归一化、通道转换这些操作还得自己在Python里做,或者额外挂OpenCV。

第三条就是我这一篇主推的方案:模型导出为ONNX,用OpenCV的dnn模块加载和推理,预处理、后处理全部用OpenCV和NumPy完成,外面再套一层FastAPI提供接口。为什么不用ONNX Runtime而用OpenCV?因为OpenCV的dnn模块已经封装好了blobFromImage,一行代码就能完成图像缩放、减均值、交换RGB通道这些操作,而且OpenCV本身是图像处理库,读图、画框、编码返回结果全是一条链,不用再引入别的依赖。而且OpenCV的DNN后端支持OpenVINO、CUDA,性能提升空间也不小。

表格对比更直观:

方案优点痛点适用场景
PyTorch直接推理开发快、调试直观部署依赖重、性能一般原型验证、内部工具
ONNX Runtime性能好、跨平台预处理集成度低高吞吐服务、边缘设备
OpenCV DNN + FastAPI预处理一体化、依赖轻算子支持有边缘case中小型业务API、快速上线

1.2 我踩过的坑:为什么最终落到FastAPI + OpenCV

之前有个项目,检测模型是用PyTorch写在内部系统里的,每天调用的量不大,当时图省事直接用FastAPI加载.pt文件跑接口。上线第一周没问题,第二周并发一上来,问题全暴露了:GPU显存被多个worker重复加载的模型副本吃满,CPU推理时GIL又锁得死死的,一个慢请求能把整个服务拖垮。后来我把模型转成ONNX,改用OpenCV DNN加载,情况立刻改善。原因有几个。

OpenCV DNN加载模型之后,Net对象可以全局只初始化一次,所有请求复用同一个推理实例,不会像PyTorch那样每个worker都启动一个模型副本。而且blobFromImage自带nchw格式转换和归一化,省掉了自己手写预处理循环的时间。FastAPI这边就更顺了,它原生支持异步接口,配合Python的线程池,可以把CPU密集型的OpenCV推理放到后台线程执行,请求进来不会互相阻塞。

更关键的是,这套方案完全不依赖具体的深度学习框架版本。只要导出的ONNX文件是对的,服务器上装一个OpenCV就能跑,环境迁移成本极低。我甚至遇到过训练环境升级了PyTorch导致旧权重加载报错的情况,换成ONNX之后,再也没为框架版本头痛过。

2. 部署前的模型准备:从pt到ONNX的转换与校验

2.1 导出ONNX的完整步骤

YOLOv11的模型结构在不同版本里可能有细微差异,但导出ONNX的思路是一致的。我用的是Ultralytics训练出来的权重,导出命令在他们的库里有现成封装,但我不推荐直接跑官方脚本出完事,一定要带着固定输入尺寸和opset版本导出。

先看命令行方式:

yolo export model=yolo11n.pt format=onnx opset=12 imgsz=640

导出完会得到yolo11n.onnx。这里有两个参数要留意:opset版本决定了ONNX算子兼容性,建议用12或13,太新的话旧版OpenCV可能不支持;imgsz固定为640,如果训练时用的分辨率不是640,请改成你训练时的实际尺寸,否则检测精度会下降。

有些场景需要在导出后做简化,把Constant折叠掉、删掉无用的Identity节点。我一般会用一个小脚本来做结构检查和简单优化:

import onnx from onnxsim import simplify model = onnx.load("yolo11n.onnx") model_simp, check = simplify(model) print("简化结果:", check) onnx.save(model_simp, "yolo11n_sim.onnx")

onnxsim不是必须的,但如果你的ONNX文件里有一堆冗余节点,OpenCV加载后推理速度会受影响。我实际测过,一个清理干净的模型比原始导出模型在CPU上能快15%左右。

2.2 用OpenCV读取ONNX的注意事项

用OpenCV加载ONNX模型非常简单:

import cv2 net = cv2.dnn.readNetFromONNX("yolo11n_sim.onnx")

但这只是开始,真正的坑在输入图像的预处理。YOLOv11训练时用的是RGB三通道图像,像素值归一化到0到1,而OpenCV读进来默认是BGR,且像素值范围是0到255。如果直接用读进来的图像塞给网络,检测结果基本是乱的。

正确的预处理代码如下:

image = cv2.imread("test.jpg") blob = cv2.dnn.blobFromImage( image, scalefactor=1.0 / 255.0, size=(640, 640), mean=(0, 0, 0), swapRB=True, crop=False ) net.setInput(blob) outputs = net.forward()

scalefactor=1/255把像素缩放到0-1区间;swapRB=True把BGR转成RGB,因为训练时的预处理就是RGB;size=(640,640)会把图像直接拉伸成正方形,如果你的训练数据用了letterbox填充而不是直接拉伸,这里就要补一个保持宽高比的填充操作。

关于letterbox这里多说一句。直接拉伸会让物体变形,模型在训练时如果用了矩形填充,部署时也必须用同样的填充方式。我的习惯是写一个专门的letterbox函数:

def letterbox(img, new_shape=640): h, w = img.shape[:2] r = min(new_shape / h, new_shape / w) new_unpad = int(round(h * r)), int(round(w * r)) dw = new_shape - new_unpad[1] dh = new_shape - new_unpad[0] dw, dh = dw // 2, dw - dw // 2, dh // 2, dh - dh // 2 if (new_unpad[1], new_unpad[0]) != (w, h): img = cv2.resize(img, (new_unpad[1], new_unpad[0])) top, bottom = int(dh[0]), int(dh[1]) left, right = int(dw[0]), int(dw[1]) img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=(114, 114, 114)) return img

注意填充值用114是Ultralytics训练时的默认值,这个细节不对齐会影响小目标召回。

3. 用FastAPI搭建推理服务的完整实现

3.1 项目目录与依赖准备

推荐的项目结构很简单,不需要复杂分层,毕竟这只是一个推理服务,不是业务后端。

deploy/ ├── app.py # FastAPI 入口 ├── model.py # 模型加载与推理 ├── preprocess.py # 图像预处理 ├── postprocess.py # 后处理解析 ├── config.py # 配置参数 ├── models/ │ └── yolo11n_sim.onnx # 模型文件 └── requirements.txt

requirements.txt里我一般只放四个核心依赖:

fastapi==0.111.0 uvicorn[standard]==0.30.1 opencv-python==4.9.0.80 numpy==1.26.4

版本号不是随便写的,OpenCV 4.9对ONNX算子支持比较全,NumPy 1.26兼容性和性能都稳定。如果你需要GPU加速,再加一个opencv-python-headless替代opencv-python,避免服务器上不必要的GUI依赖。

3.2 核心代码:预处理、推理、后处理

预处理的代码我直接把letterbox和blobFromImage合并到一个模块里。这里有一个小技巧:把模型输入尺寸和letterbox的缩放比例记录下来,后处理时要靠它把检测框坐标映射回原图。

# preprocess.py import cv2 import numpy as np class Preprocessor: def __init__(self, input_size=640): self.input_size = input_size def __call__(self, image_bgr: np.ndarray): return self.preprocess(image_bgr) def preprocess(self, image_bgr): h, w = image_bgr.shape[:2] # letterbox 填充到 640x640 padded = np.full((self.input_size, self.input_size, 3), 114, dtype=np.uint8) r = min(self.input_size / h, self.input_size / w) new_w, new_h = int(round(w * r)), int(round(h * r)) resized = cv2.resize(image_bgr, (new_w, new_h), interpolation=cv2.INTER_LINEAR) padded[:new_h, :new_w] = resized blob = cv2.dnn.blobFromImage(padded, 1.0 / 255.0, (self.input_size, self.input_size), swapRB=True) return blob, r

推理模块加载模型后,把所有请求共享的模型放在全局,避免重复加载:

# model.py import cv2 import numpy as np class YOLOInference: def __init__(self, onnx_path: str, input_size: int = 640, conf_thres: float = 0.25, iou_thres: float = 0.45): self.net = cv2.dnn.readNetFromONNX(onnx_path) self.input_size = input_size self.conf_thres = conf_thres self.iou_thres = iou_thres def infer(self, blob: np.ndarray): self.net.setInput(blob) outputs = self.net.forward() return outputs

后处理可能是整个链路里最麻烦的部分。YOLOv11的输出通常是一个形状为(1, 84, 8400)的张量,其中84表示4个框坐标加80个类别概率,8400表示三个尺度下的anchor数量。OpenCV的forward()返回的是(1, 84, 8400)还是(1, 8400, 84)取决于导出时的transpose设置,这一步最容易出错。

我统一写成既支持两种排列的解析函数:

# postprocess.py import numpy as np def postprocess(outputs, orig_shape, scale, conf_thres=0.25, iou_thres=0.45): if len(outputs.shape) == 3: if outputs.shape[1] == 84: outputs = outputs[0].T # 转成 (8400, 84) else: outputs = outputs[0] # 已经是 (8400, 84) boxes = [] scores = [] class_ids = [] for pred in outputs: conf = pred[4:] class_id = int(np.argmax(conf)) score = float(conf[class_id]) if score < conf_thres: continue cx, cy, bw, bh = pred[:4] # 映射回原图坐标 cx = (cx - 0) / scale cy = (cy - 0) / scale bw = bw / scale bh = bh / scale x1 = cx - bw / 2 y1 = cy - bh / 2 x2 = cx + bw / 2 y2 = cy + bh / 2 boxes.append([x1, y1, x2, y2]) scores.append(score) class_ids.append(class_id) if not boxes: return [] # 用 OpenCV NMS 去除重叠框 inds = cv2.dnn.NMSBoxes(boxes, scores, conf_thres, iou_thres) results = [] for idx in inds: i = idx[0] if isinstance(idx, np.ndarray) else idx results.append({ "box": boxes[i], "score": scores[i], "class_id": class_ids[i] }) return results

注意NMS这一步用OpenCV的dnn.NMSBoxes而不是自己写循环,因为OpenCV底层是C++实现,性能比Python循环快一个量级。

3.3 异步接口与并发控制

FastAPI默认是异步框架,但我的推理函数是CPU密集型的同步操作,如果直接放在async函数里会阻塞事件循环。正确的做法是使用run_in_executor把同步推理丢到线程池里执行,或者直接定义同步的def接口,FastAPI会自动把它放到线程池跑。

我推荐后者,代码更简洁:

# app.py import time import asyncio from fastapi import FastAPI, UploadFile, File, HTTPException import cv2 import numpy as np from preprocess import Preprocessor from model import YOLOInference from postprocess import postprocess app = FastAPI(title="YOLOv11推理服务") model_path = "models/yolo11n_sim.onnx" preprocessor = Preprocessor(input_size=640) yolo = YOLOInference(model_path, conf_thres=0.25, iou_thres=0.45) @app.post("/predict") def predict(file: UploadFile = File(...)): image_bytes = file.file.read() np_arr = np.frombuffer(image_bytes, np.uint8) image_bgr = cv2.imdecode(np_arr, cv2.IMREAD_COLOR) if image_bgr is None: raise HTTPException(status_code=400, detail="图片解码失败") blob, scale = preprocessor.preprocess(image_bgr) outputs = yolo.infer(blob) results = postprocess(outputs, image_bgr.shape[:2], scale) return results

这个接口收到上传图片后,返回一个JSON数组,每个元素包含box、score和class_id。对大多数业务场景来说,这个格式足够前端绘制检测框用了。

但是并发控制不能忽略。OpenCV的Net对象是线程不安全的,多个线程同时调用同一个net.forward()可能崩溃或结果错乱。我在代码里必须加锁。用threading.Lock包住推理调用:

import threading infer_lock = threading.Lock() def safe_infer(blob): with infer_lock: return yolo.infer(blob)

锁住之后并发能力会受限制,但至少不会出错。如果确实需要高并发,后面会讲多模型副本的方案。

3.4 配置与启动

启动服务用uvicorn:

uvicorn app:app --host 0.0.0.0 --port 8000 --workers 1

这里特别注意:--workers不要设置成多个,除非你在每个worker里都能承受加载一份模型副本。OpenCV加载ONNX模型内存占用不高但也不能忽略,一个yolo11n模型大约几十MB,workers开太多内存就爆了。更合理的做法是单worker + 线程池,配合前面的锁确保线程安全。

如果要用多进程,建议给每个进程分配独立的GPU设备,或者干脆用后面说的模型副本池。

4. 性能优化与稳定性设计

4.1 预热模型与识别Tab

很多人在测试时发现第一次请求特别慢,要等好几秒。原因是模型加载、图像解码的JIT编译、OpenCV的CPU指令集适配都发生在第一次调用时。生产环境一定要做预热。

FastAPI的startup事件里跑一次空推理:

from contextlib import asynccontextmanager @asynccontextmanager async def lifespan(app: FastAPI): # 预热:用一张纯色图跑一次推理 dummy = np.full((640, 640, 3), 114, dtype=np.uint8) blob, _ = preprocessor.preprocess(dummy) yolo.infer(blob) print("模型预热完成") yield app = FastAPI(lifespan=lifespan)

这一步能消除大部分首次请求延迟。我实测过,不预热的第一个请求耗时约2.3秒,预热后稳定在80毫秒左右。

4.2 批处理与排队

业务方经常一次性传很多图片,如果每张图都单独请求接口,网络开销和HTTP开销会占大头。我通常会提供两个接口:单图/predict和批量/predict_batch,批量接口内部循环所有图片,共享一次数据解码和模型加载的摊销成本。

核心是控制最大批大小,避免一张超大batch把内存打满。我的建议是单次batch不超过16张图,超过就分批。

如果在高并发场景,可以引入内存队列,把请求图片放入队列,后台线程批量取出并推理。FastAPI配合asyncio.Queue可以做到:

import asyncio from collections import deque request_queue = asyncio.Queue(maxsize=64) async def worker(): while True: batch = [] # 最多等0.1秒凑一批,或者凑满8张就推理 while len(batch) < 8: try: item = await asyncio.wait_for(request_queue.get(), timeout=0.1) batch.append(item) except asyncio.TimeoutError: break if batch: # 线程池里推理 results = await asyncio.to_thread(batch_infer, batch) for item, result in zip(batch, results): item["future"].set_result(result)

这个模式比较复杂,但能显著提高吞吐。不过如果业务QPS不高,普通单图接口已经足够,不必过度设计。

4.3 超时、异常与优雅重启

推理服务最容易出现的生产事故是:输入图片异常导致OpenCV内部崩溃,或者模型推理抛异常没有被捕获,导致整个进程退出。我给自己定了几条硬规矩。

第一,上传图片大小限制。FastAPI里加max_size,或者在读取时检查文件大小:

file_size = 0 while True: chunk = await file.read(1024 * 1024) file_size += len(chunk) if file_size > 5 * 1024 * 1024: raise HTTPException(status_code=413, detail="图片太大") if not chunk: break

第二,推理调用包一层异常捕获,记录日志并返回503,而不是让500错误直接暴露堆栈:

try: outputs = yolo.infer(blob) except Exception as e: raise HTTPException(status_code=503, detail=f"模型推理失败: {str(e)}")

第三,超时控制。用asyncio.timeout或者中间件给每个请求设定最大处理时间,例如2秒。超过就返回408,避免客户端一直挂着连接。

4.4 性能测试与调优实践

我常用的压测工具是locust或者简单的wrk。先用curl确认接口正常,再压测:

curl -X POST http://127.0.0.1:8000/predict -F "file=@test.jpg" wrk -t4 -c20 -d30s -s post.lua http://127.0.0.1:8000/predict

压测时重点观察几个指标:p50、p95延迟,错误率,以及CPU使用率。如果发现CPU跑满但吞吐上不去,优先检查后处理里的Python循环是不是瓶颈。我有个项目就是后处理里用了纯Python遍历8400个候选框,消耗了总延迟的一大半。优化方式是先用NumPy向量化筛选,再做NMS,延迟从120毫秒降到70毫秒。

一个向量化的例子:

conf = pred[:, 4:] max_conf = conf.max(axis=1) class_ids = conf.argmax(axis=1) valid = max_conf >= conf_thres if valid.sum() == 0: return [] pred = pred[valid] scores = max_conf[valid] class_ids = class_ids[valid]

这只是第一步,后续还可以把坐标反算、缩放这些步骤也向量化。

5. 常见问题与排查技巧实录

5.1 推理结果全为零或检测框错乱

这个问题最常见的原因就是预处理设置不对。我遇到过三次,每一次都是因为swapRB没有打开,导致输入图像的颜色通道顺序和训练时不一致。表现为:检测框位置大致对,但置信度很低,或者检测到完全错误的类别。

排查顺序:先用一张标准测试图,分别用PyTorch原始模型和OpenCV部署模型跑一遍,对比输出。如果两者差异巨大,优先检查blobFromImage的参数。另外还需要确认letterbox的填充值,Ultralytics用的是114,如果你训练时用了别的填充值,必须保持一致。

还有一个隐蔽问题:cv2.dnn.blobFromImage默认输出格式是NCHW,如果你的模型导出时加了transpose操作变成了NHWC,就需要在setInput时指定blob的shape,或者调整blobFromImage的输出顺序。最简单的方式是打印一下outputs.shape,看一下第二维是84还是8400,然后把后处理的转置逻辑对应上。

5.2 内存泄漏与显存问题

OpenCV DNN在CPU模式下很少漏内存,但如果用了CUDA后端,可能会遇到显存不释放的情况。我遇到过一次:连续推理几百张图之后,显存占用逐渐涨高,最后CUDA out of memory。

原因是我每次推理时都调用了net.setInput并创建新的blob,OpenCV的CUDA后端会在内部为每个输入尺寸分配缓存。解决办法是固定输入尺寸,不要随便改变size。如果你必须处理多种分辨率,建议统一先让所有图letterbox到640,而不是让网络接收动态尺寸。另外可以调用net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)之前设置cv2.dnn.DNN_TARGET_CUDA_FP16,但前提是你的OpenCV编译时带上了CUDA支持,否则直接报错。

5.3 请求量一大就超时

先排查是不是锁竞争。如果我的infer_lock把整个推理串行化了,并发8个请求时后面几个都要排队等锁,响应时间自然飙升。解决办法有几个:

  • 换成多模型副本池,每个副本一把锁,请求随机分配到空闲副本;
  • 用批处理模式,把并发请求合并成batch,一次性推理;
  • 升级到GPU推理,把OpenCV后端切到CUDA,单个推理延迟从80ms降到20ms。

我最常用的是多副本池,实现不复杂:

class InferencePool: def __init__(self, onnx_path, pool_size=4): self.inferers = [YOLOInference(onnx_path) for _ in range(pool_size)] self.locks = [threading.Lock() for _ in range(pool_size)] self.cursor = 0 def infer(self, blob): for _ in range(len(self.inferers)): idx = self.cursor % len(self.inferers) self.cursor += 1 lock = self.locks[idx] if lock.acquire(blocking=False): try: return self.inferers[idx].infer(blob) finally: lock.release() # 全忙,就阻塞等第一把锁 with self.locks[self.cursor % len(self.inferers)]: return self.inferers[self.cursor % len(self.inferers)].infer(blob)

这样可以让多个请求真正并行推理,前提是CPU有多核。

5.4 模型版本更新时如何平滑切换

业务方经常要更新模型,你不能直接删掉旧文件再加载新的,否则接口会有一段时间不可用。我的做法是:在配置里维护一个当前模型路径,启动时加载;需要更新时,先把新模型文件写到临时目录,校验一下能正常推理,再通过一个管理接口切换当前模型路径,并且把新的推理对象加载好,旧对象等所有正在进行的推理结束之后释放。

实现方案:

class ModelManager: def __init__(self, primary_path, fallback_path=None): self.current = YOLOInference(primary_path) self.fallback = YOLOInference(fallback_path) if fallback_path else None self.switching = False def reload(self, new_path): if self.switching: raise RuntimeError("正在切换中") self.switching = True try: new_model = YOLOInference(new_path) old = self.current self.current = new_model # 等旧模型不再使用时可释放,简化处理就是直接让GC回收 del old finally: self.switching = False

生产变化必须配合监控。

6. 生产环境部署的进一步扩展

6.1 容器化部署要点

部署推理服务最好用Docker封装,环境一致性省心。我的Dockerfile一般分两段:先用Python镜像装依赖,再复制模型文件和代码。注意OpenCV的依赖比较重,如果用了opencv-python-headless,镜像能小不少。

FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

启动容器时限制CPU和内存:

docker run -d --name yolo-api -p 8000:8000 --cpus=2 -m 2g yolo-api:latest

限制资源不是坏事,反而能防止模型推理把服务器全部CPU抢光,影响同一台机器上的其他服务。

6.2 监控与告警

别以为服务能跑就万事大吉。我给这个推理服务加了三个监控指标:每个请求的处理延迟、在途请求数、模型推理失败次数。用Prometheus和Grafana一套下来,在fastapi里集成prometheus_client就很方便。

from prometheus_client import Histogram, Counter, Gauge import time REQUEST_TIME = Histogram("request_processing_seconds", "Time spent processing request", buckets=(0.01, 0.05, 0.1, 0.5, 1)) REQUEST_COUNT = Counter("request_count", "Total requests") INFER_FAILURE = Counter("infer_failure", "Infer failures") @app.post("/predict") def predict(file: UploadFile = File(...)): start = time.time() REQUEST_COUNT.inc() try: ... except Exception: INFER_FAILURE.inc() raise finally: REQUEST_TIME.observe(time.time() - start)

告警规则也别设得太复杂,我经验是延迟p95超过500ms、错误率超过1%持续5分钟就发告警,足够提前发现问题了。

6.3 模型加速选项

如果CPU推理满足不了性能要求,优先考虑OpenVINO后端,而不是直接上GPU。OpenVINO是Intel CPU上的推理加速库,OpenCV DNN原生支持,改动极小:

net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENVINO) net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)

这个后端在Intel的机器上速度提升非常明显,yolo11n的CPU推理从80ms可以降到40ms左右。如果必须在NVIDIA GPU上跑,那就需要确保OpenCV是用CUDA编译的,然后设置:

net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA) net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA_FP16)

注意CUDA后端对模型算子的支持不如CPU全,有些YOLOv11的算子可能在CUDA环境下报错。实际用之前要全面测试一遍,我经历过ONNX里一个Resize算子在CUDA上不支持导致直接抛异常的情况,最后只能退回CPU推理。

写在最后

部署这一层,代码只是其中一半,另一半是对模型行为、框架特性、运维状态的理解。我自己把YOLOv11跑进生产环境后最大的感受是:花的精力比预想的多得多,但一旦体系搭好,后续所有模型迭代都只是换一个ONNX文件的事。OpenCV负责图像处理和推理,FastAPI负责对外暴露接口,两个工具配合起来,既轻量又够用。

最后送一个实战小技巧:每次发布模型前,先在本地用同一张测试图跑一次接口,把返回的JSON保存下来,之后再改任何代码或模型,回来对比这张图的检测结果。输出一致,说明部署链路没被改坏。这个习惯帮我避免过很多次“莫名其妙上线就出错”的尴尬。后面第009篇我会继续写YOLOv11的模型量化与边缘设备部署,到时再见。

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

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

立即咨询