☰
C++部署YOLO11分类模型:ONNX Runtime CPU推理全流程解析
2026/10/11 14:28:43 网站建设 项目流程

简介:面向需要在Windows CPU环境部署YOLO11图像分类模型的开发者,这份资料提供了一套纯C++实现的ONNX Runtime推理方案,无需GPU即可完成模型加载、预处理、推理与后处理全流程。工程代码支持直接替换自定义ONNX模型(兼容YOLOv8/v11等架构),并针对多线程进行加速优化,实测Intel i5-12400F单帧推理约120ms,适合边缘设备、性能验证及C++项目集成等场景。包体共365个文件,以C++头文件(hpp/h)为主,配合可执行程序、动态链接库、CMake构建脚本及ONNX模型文件,压缩包约363MB。同时附有环境配置指南、API接口说明与常见问题排查文档,源码目录结构清晰,便于直接移植或二次开发。目前已有104人学习,适合希望绕过Python依赖、快速验证模型CPU端性能或将其嵌入现有C++系统的开发者。

1. cppYolo11OnnxPredict.zip 解决什么问题:Windows 无 GPU 机器上的 yolo11 图像分类落地

给已有的 Windows 桌面程序加一个「看一眼图片就给出类别」的能力,最省事的路径就是标题里这套组合:模型用 yolo11 分类头,训练好的权重导出成 ONNX,再由 C++ 调用 ONNX Runtime 在纯 CPU 上完成推理。这个 zip 工程的价值在于,它把 PyTorch 侧的事做完之后,Windows 侧只剩一个 C++ 可执行程序和几个资源文件,不带 Python 运行时、不挑显卡,放到工控机和普通办公机上都能跑。适合两类人:一类是想给 C++ 产品快速加图像分类能力的开发,另一类是已经在 Python 里训好模型、正头疼怎么交付给客户现场的人。下面从导出 ONNX 开始,一路讲到 C++ 推理链路的每个参数和替换模型时的坑。

2. 先把导出链路立住:从 yolo11 网络结构到 pytorch 转 onnx 的关键参数

2.1 yolo11 分类网络结构与 CPU 部署的匹配点

yolo11 网络结构里,分类任务(-cls)和检测任务共享骨干网络的思路,但去掉了检测头里一大堆输出分支,最后接的是全局平均池化加一个全连接分类头。这意味着它的输出就是一个形状为[batch, num_classes]的二维张量,不需要做 NMS、不需要解析 anchor,推理后处理可以简化为一次 softmax。

这个结构对 CPU 部署非常友好。检测模型在 CPU 上跑,瓶颈通常不在骨干网络,而在多尺度输出和候选框后处理;分类模型把这些都省了,一次前向就完事。以 yolo11n-cls 这种轻量级权重为例,224×224 输入在普通 i5 上单线程推理大约几十毫秒,多线程可以压到十几毫秒,完全能满足图片分类这种非实时的交付场景。如果换更大规模的 yolo11m-cls 甚至 x-cls,CPU 推理就会明显吃力,这时候要考虑降低输入尺寸或者压缩到 ONNX 后做量化。

还有一个容易被忽略的匹配点:分类模型在训练时用的是 ImageNet 那套归一化参数(mean = 0.485/0.456/0.406,std = 0.229/0.224/0.225),而不是检测常用的 0-1 缩放。C++ 侧复刻预处理时必须和训练侧严格一致,这是后面所有精度问题的根源。

2.2 导出 ONNX:opset、动态轴和输入输出命名

拿到训练好的.pt权重后,第一步是把它转成 ONNX。这里不建议直接用 ultralytics 自带的 export 接口,虽然它也能导出,但输入输出名字和一些细节自己控制不了,后面 C++ 侧对接时容易来回改。我一般直接在导出脚本里用torch.onnx.export,把名字、opset、动态轴都写清楚:

# export_cls_onnx.py —— 把 yolo11 分类模型导出为 ONNX # 依赖:pip install ultralytics onnxruntime import torch from ultralytics import YOLO model = YOLO("yolo11n-cls.pt") model.model.eval() # 分类头输出 logits,不是 softmax 后的概率 dummy = torch.randn(1, 3, 224, 224) torch.onnx.export( model.model, # 注意是 model.model,绕开 ultralytics 的导出封装 dummy, "yolo11n-cls.onnx", input_names=["images"], # 这个名字后面 C++ 代码里要一字不差地复用 output_names=["output0"], opset_version=17, # 覆盖 CPU 上常见算子足够了 dynamic_axes={"images": {0: "batch"}, "output0": {0: "batch"}}, ) print("export done")

执行完python export_cls_onnx.py后,先别急着写 C++,用 ONNX Runtime 的 Python 接口读一遍模型,确认输入输出的形状和名字:

# check_model.py —— 打印 ONNX 的输入输出信息 import onnxruntime as ort sess = ort.InferenceSession("yolo11n-cls.onnx", providers=["CPUExecutionProvider"]) for i in sess.get_inputs(): print("input:", i.name, i.shape, i.type) for o in sess.get_outputs(): print("output:", o.name, o.shape, o.type)

opset 参数说明:opset 17 在较新的 ONNX Runtime 上都能无脑支持,如果导出时报某些算子不支持,优先降到 15 或 16,而不是去升级 ONNX Runtime。dynamic_axes 里只把 batch 维度设为动态就够了,宽高保持静态,这样 C++ 侧不用每次重新分配推理内存,CPU 上的速度和内存占用都可控。注意导出的模型内部如果混入了 float64 节点(比如某些自定义 loss 留下的痕迹),Session 创建时会直接报类型错误,导出前用model.model.float()统一转一次。

2.3 ONNX Runtime 的 CPU 执行链:线程数、优化级别与首帧预热

模型落地到 C++ 之前,先理解 ONNX Runtime 在 CPU 上是怎么跑的。它默认的 CPUExecutionProvider 会把图做常量折叠、算子融合,再分派给底层算子库。对单模型推理来说,真正要调的只有几个 SessionOptions 参数:

// init_session.cpp —— Session 初始化的固定写法 #include <onnxruntime_cxx_api.h> Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "yolo11-cls"); Ort::SessionOptions so; so.SetGraphOptimizationLevel(GRAPH_OPTIMIZATION_LEVEL_LEVEL3); // 全量图优化 so.SetIntraOpNumThreads(4); // 每个算子内部并行线程数 so.SetInterOpNumThreads(1); // 单模型单张图,不需要算子间并行

参数怎么定,我给出常用的取值和理由:

参数推荐值说明
SetGraphOptimizationLevelLEVEL3等价于 ORT_ENABLE_ALL,包含算子融合、常量折叠,CPU 上收益明显
SetIntraOpNumThreads物理核数的一半到全部工控机建议 4,注意 ONNX Runtime 会抢占 CPU,影响同机 GUI
SetInterOpNumThreads1单模型场景设大了反而带来线程切换开销
SetMemPattern默认保持默认即可,CPU 内存带宽是瓶颈时再考虑关闭

这里有个实战经验:第一次调用session.Run时,ORT 才真正执行图优化和算子编译,所以第一张图的耗时往往是后面的几倍。工程里应该在Init结束时用一张全灰的 dummy 图跑一次预热,把几百毫秒的初始化开销从用户感知的首次识别里挪走。这个坑在后面的避坑章节还会出现一次,因为它最隐蔽。

3. C++ 推理代码怎么搭:Session 初始化、预处理与 Top-5 后处理

3.1 工程骨架:目录、依赖和 Session 初始化

一个能交付的 cpp 工程,目录里至少要有这几样:predictor.h/predictor.cpp放推理类,main.cpp做调用演示,models/yolo11n-cls.onnx放模型,labels.txt放类别名,还有onnxruntime.dll和opencv_world*.dll跟着 exe 走。编译链接时引入onnxruntime.lib,运行时 dll 放 exe 同目录,这是最不容易出问题的布局。

推理类的头文件我一般这样写:

// predictor.h —— 分类推理类的对外接口 #pragma once #include <onnxruntime_cxx_api.h> #include <opencv2/opencv.hpp> #include <vector> #include <string> class ClsPredictor { public: bool Init(const std::string& model_path, const std::string& labels_path); int Predict(const cv::Mat& bgr_img, float* confidence); private: std::vector<float> Preprocess(const cv::Mat& bgr_img); std::vector<std::pair<int, float>> TopKSoftmax(const float* logits, int n, int k); Ort::Env env{nullptr}; Ort::Session session{nullptr}; std::vector<std::string> labels_; int input_h_ = 224; int input_w_ = 224; bool use_letterbox_ = true; float mean_[3] = {0.485f, 0.456f, 0.406f}; float std_[3] = {0.229f, 0.224f, 0.225f}; };

Init 里除了建 Session,还要把输入尺寸从模型里读出来,而不是写死,这是后面「可直接替换模型」的前提:

// predictor.cpp —— Session 初始化与输入尺寸读取 #include <fstream> #include <numeric> bool ClsPredictor::Init(const std::string& model_path, const std::string& labels_path) { env = Ort::Env(ORT_LOGGING_LEVEL_WARNING, "yolo11-cls"); Ort::SessionOptions so; so.SetGraphOptimizationLevel(GRAPH_OPTIMIZATION_LEVEL_LEVEL3); so.SetIntraOpNumThreads(4); session = Ort::Session(env, model_path.c_str(), so); // 从模型元信息里读取输入尺寸,不依赖写死的 224 auto in_info = session.GetInputTypeInfo(0); auto in_shape = in_info.GetTensorTypeAndShapeInfo().GetShape(); if (in_shape.size() == 4) { // NCHW input_h_ = static_cast<int>(in_shape[2]); input_w_ = static_cast<int>(in_shape[3]); } // labels 文件:每一行一个类别名,行号就是类别 id std::ifstream f(labels_path); std::string line; while (std::getline(f, line)) { if (!line.empty()) labels_.push_back(line); } if (labels_.empty()) return false; return true; }

两个容易踩的点补充一下。第一,GetInputShape返回的宽高在导出时如果用了动态轴,可能是 -1,这时要回退到成员变量默认值,不能直接拿去分配内存。第二,Windows 上模型路径带中文时,const char*的 Session 构造重载按 UTF-8 解析会失败,改用它提供的wchar_t*重载传宽字符串,或者在上游先把路径转码。

3.2 预处理:letterbox、BGR 转 RGB 与归一化的一次性实现

预处理是整条链路里翻车率最高的环节,而且翻车方式不是报错,是精度悄悄掉。yolo11 分类训练时用的是「等比缩放 + 补边」还是「直接拉伸」,取决于训练脚本里的 transforms;如果训练时用了 letterbox,推理时改成直接 resize,Top-1 会掉得让你怀疑模型是坏的。C++ 侧我把两个开关都做出来,用配置文件控制:

// predictor.cpp —— 预处理:letterbox、通道转换、归一化 std::vector<float> ClsPredictor::Preprocess(const cv::Mat& bgr_img) { cv::Mat rgb; cv::cvtColor(bgr_img, rgb, cv::COLOR_BGR2RGB); cv::Mat canvas; if (use_letterbox_) { float scale = std::min((float)input_w_ / rgb.cols, (float)input_h_ / rgb.rows); int nw = (int)std::round(rgb.cols * scale); int nh = (int)std::round(rgb.rows * scale); cv::Mat resized; cv::resize(rgb, resized, cv::Size(nw, nh), 0, 0, cv::INTER_LINEAR); canvas = cv::Mat::zeros(input_h_, input_w_, CV_8UC3); resized.copyTo(canvas(cv::Rect((input_w_ - nw) / 2, (input_h_ - nh) / 2, nw, nh))); } else { cv::resize(rgb, canvas, cv::Size(input_w_, input_h_), 0, 0, cv::INTER_LINEAR); } // HWC 转 CHW,同时做归一化,一次遍历完成 std::vector<float> tensor(3 * input_h_ * input_w_); for (int c = 0; c < 3; c++) { for (int i = 0; i < input_h_; i++) { for (int j = 0; j < input_w_; j++) { cv::Vec3b p = canvas.at<cv::Vec3b>(i, j); float v = p[c]; // 因为前面转了 RGB,c=0 是红通道 tensor[c * input_h_ * input_w_ + i * input_w_ + j] = (v / 255.0f - mean_[c]) / std_[c]; } } } return tensor; }

这段代码的逻辑说明:先把 OpenCV 读进来的 BGR 转成 RGB 亮序,再做空间缩放,最后在填充张量时按c * h * w + i * w + j的下标把数据摆成 ONNX 期望的 NCHW 布局。mean 和 std 的取值必须和导出的训练脚本一致,如果你的模型是用 0-1 归一化训练的,把 mean 设为 0、std 设为 1 即可。三重循环在 224×224 下大约 15 万次浮点运算,耗时可以忽略,但如果你要压到极致,可以改成按行指针顺序扫描并配合霓虹指令,性能瓶颈不在这里。

3.3 后处理:softmax 与 Top-5 的稳定写法

模型输出的是 logits,不是概率,所以要先做 softmax,再取前 k 个类别。数值稳定性上,先减最大值再 exp,可以避免大 logits 时 exp 溢出:

// predictor.cpp —— softmax + Top-5 std::vector<std::pair<int, float>> ClsPredictor::TopKSoftmax(const float* logits, int n, int k) { float maxv = *std::max_element(logits, logits + n); std::vector<float> expv(n); float sum = 0.f; for (int i = 0; i < n; i++) { expv[i] = std::exp(logits[i] - maxv); sum += expv[i]; } std::vector<std::pair<int, float>> items(n); for (int i = 0; i < n; i++) items[i] = {i, expv[i] / sum}; std::partial_sort(items.begin(), items.begin() + k, items.end(), [](const auto& a, const auto& b) { return a.second > b.second; }); items.resize(k); return items; }

std::partial_sort只保证前 k 个有序,复杂度是 O(n log k),对 1000 类输出来说比全排序省不少时间。拿到结果后,用items[0].first去查labels_就是类别名。整个 Predict 函数把前面三件事串起来,调用session.Run时输入张量的名字必须和导出时保持一致(我们导出用的是 "images" 和 "output0"):

int ClsPredictor::Predict(const cv::Mat& bgr_img, float* confidence) { auto input = Preprocess(bgr_img); Ort::MemoryInfo mi = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); std::vector<int64_t> shape{1, 3, input_h_, input_w_}; Ort::Value tensor = Ort::Value::CreateTensor<float>( mi, input.data(), input.size(), shape.data(), shape.size()); const char* in_names[] = {"images"}; const char* out_names[] = {"output0"}; auto outputs = session.Run(Ort::RunOptions{nullptr}, in_names, &tensor, 1, out_names, 1); float* logits = outputs[0].GetTensorMutableData<float>(); auto shape_out = outputs[0].GetTensorTypeAndShapeInfo().GetShape(); int n_class = (int)shape_out[1]; auto top = TopKSoftmax(logits, n_class, 5); *confidence = top[0].second; return top[0].first; }

注意input这个 vector 的生命周期要覆盖到session.Run返回,CreateTensor不会拷贝数据,只是持有指针。如果你在函数里把 vector 提前析构了,会读到野指针,这种偶发崩溃很难查。

4. 可直接替换模型:换 ONNX 之前的三个校验步骤与两处联动改动

4.1 替换前校验:用 Python 打印 ONNX 的输入输出信息

标题里「可直接替换模型」是这套代码的卖点,但「直接」是有边界的:只要新模型满足输入名images、输出名output0、布局 NCHW、类别数与 labels 文件行数一致,就能做到只换文件不编译。替换前先跑一遍上一章写过的check_model.py,把下面三点对着看:

  • 输入名是否叫images,形状是不是[batch, 3, 224, 224]或[-1, 3, 224, 224];
  • 输出名是否叫output0,形状第二维是不是你要的类别数;
  • 输入类型是不是tensor(float),如果显示double,说明导出时模型权重没转 float32。

任何一个不满足,处理方式都不同:输入名不对就在新模型重新导出时改名,或者在 C++ 上下文里把in_names改掉;类别数不对就去检查 labels 文件;类型是 double 的话必须回 PyTorch 重新导出,不能在 C++ 侧强行转。

4.2 两处联动改动:输入尺寸读取与 labels 行数对齐

新模型如果输入尺寸不是 224,比如用了 320,代码里的处理已经做了:Init 时从模型元信息读取input_h_和input_w_,预处理和后处理自动跟随。但有一个前提,导出时不要把宽高也设置成动态轴,只动 batch 就行。如果新模型尺寸超出 CPU 能接受的延迟预算,优先改的是训练侧的输入尺寸,而不是代码。

labels 行数对齐是「能跑但结果全错」的高发区。比如原模型是 1000 类 ImageNet,你换了 10 类的微调模型,忘了更新 labels.txt,代码不报错,但 Top-1 返回的类别 id 会指向错误的类别名。Init 里读 labels 时,我建议加一道校验:读完后如果labels_.size()和 Session 输出的第二维对不上,直接 Init 失败并打印日志,把静默错误变成显式错误。这个习惯能省掉后面排障的好几个小时。

除了行数,还有预处理一致性。不同训练脚本对前处理的设定可能完全不同:有人对输入先做 0-1 缩放,有人直接减均值除方差;有人用 letterbox,有人用 resize。替换模型后第一件事不是看 C++ 跑得快不快,而是拿一张训练集里没见过的图,把 Python 侧 ONNX Runtime 的结果和 C++ 的结果并排打出来,确认 Top-5 一致,再谈性能。

4.3 替换后的基准验证:把 Python 输出当标尺

我每次换模型,都会先跑一个基准脚本,把模型的参考输出保存下来,当作回归测试的标尺。这个脚本和 C++ 共用同一份预处理逻辑,用 numpy 实现:

# verify_baseline.py —— 生成参考输出,用来验收 C++ 侧 import numpy as np, cv2 import onnxruntime as ort def preprocess(path, size=224): img = cv2.imread(path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (size, size)) x = img.astype(np.float32) / 255.0 mean = np.array([0.485, 0.456, 0.406], dtype=np.float32) std = np.array([0.229, 0.224, 0.225], dtype=np.float32) x = (x - mean) / std return x.transpose(2, 0, 1)[None] sess = ort.InferenceSession("new_model.onnx", providers=["CPUExecutionProvider"]) x = preprocess("test_sample.jpg") ref = sess.run(["output0"], {"images": x})[0] np.save("ref_output.npy", ref) print("top5:", np.sort(ref[0])[::-1][:5])

把ref_output.npy里的 top-5 和 C++ 打印的 top-5 对比,前 3 个类别 id 一致、概率差在 1e-2 以内,就算验收通过。如果 C++ 和 Python 结果对不上,基本不用怀疑推理引擎,先查预处理:通道顺序、letterbox 开关、mean/std 三件事逐一对。这个脚本记得留在工程里,它是后面做 INT8 量化时唯一可信的评估工具。

5. 常见问题与排查:CPU 部署 yolo11 分类最容易翻车的 5 个点

5.1 现象一:精度掉得离谱——预处理不一致

现象:模型在 Python 里验证 Top-1 有 98%,C++ 部署后只有 60%,而且网络结构、模型文件都是同一份。

原因:预处理不一致。最常见的是 OpenCV 读图默认 BGR,而模型训练时是 RGB,漏了cvtColor;其次是训练用了 letterbox,C++ 侧直接 resize;再就是归一化参数写错。这三种错误都不会报错,只会让精度悄悄崩。

解决:用 4.3 的 verify_baseline.py 做二分定位。把 Python 侧预处理改成和 C++ 完全一致,如果 Python 精度也掉下来,说明问题在前处理;然后把 C++ 侧逐步改成和训练一致。我遇到最多的场景是别人给我的模型是用官方 ImageNet 均值训练的,而我看他代码以为是 0-1,参数一对,问题立现。

5.2 现象二:C++ 与 Python 结果差一个类别

现象:Top-1 相同,Top-5 顺序略有不同,或者概率相差几个百分点,主类别恰好卡在边界上。

原因:float32 累加顺序不同,加上 resize 插值在 OpenCV C++ 和 Python 底层实现有细微差异,导致 logits 有 1e-4 量级的浮动。这是正常的,不是 bug。但如果概率差超过 1e-2,就要回查预处理是否逐像素一致。

解决:先做浮点容忍度评估——两个结果概率差小于 1e-3 就放行;差异大就把 CPU 线程数临时设成 1、固定INTER_LINEAR,排除算子并行带来的乱序求和。还有一个经验:分类边界样本本来就接近两类概率平分,这时候 Top-1 变化不代表部署出错,看 Top-5 是否稳定。

5.3 现象三:Session 创建抛 BadCast / opset 异常

现象:session.Run或 Session 构造时报Bad cast、Unsupported opset version、Input does not have type float之类错误。

原因:三种可能——模型用了比你本地 ONNX Runtime 更高的 opset;模型图里有自定义算子或 float64 节点;onnxruntime.dll 版本和 lib 版本不匹配。最后一种在 Windows 上最常见,dll 更新了但代码链接的还是旧 lib。

解决:先用 Python 的 ORT 加载同一个模型,能加载就不是模型问题;报 opset 就把导出的opset_version降到 15 或 16 重新导出;报类型就回 PyTorch,model.model = model.model.float()后重新导。dll/lib 版本不一致,去 NuGet 拉同一个版本号搭配引用,不要混。

5.4 现象四:第一张图特别慢,卡顿感明显

现象:程序启动后第一次识别要 300~800ms,后续每张只要 20ms,用户觉得首帧卡。

原因:ONNX Runtime 的图优化和算子分配是懒加载的,第一次Run才真正执行。再加上 Windows Defender 首次扫描新 dll 的开销,首帧慢是双重叠加。

解决:Init 结束前用一张 224×224 的全灰图跑一次 Predict,预热 Session。这不算浪费——整个初始化成本从「用户等待第一次识别」转移到「程序启动过程」,体验好得多。另外把 onnxruntime.dll 和 exe 放同目录、排除杀软扫描目录,也能去掉一部分首帧抖动。

5.5 现象五:中文路径读不到模型或图片

现象:模型路径或图片路径含中文时,Session 创建失败或imread返回空,路径全是英文时一切正常。

原因:Ort::Session的const char*重载按 UTF-8 解释路径,Windows 下传进去的是本地代码页(GBK)字节流,就找不到文件。OpenCV 的imread同理,它不接受宽字符。

解决:模型路径用wchar_t*重载传宽字符串;图片读取改用std::ifstream把文件读成字节,再cv::imdecode。我在工程里统一加了一个PathToWstring的封装,所有文件操作都走它,中英文路径通吃。这个坑在交付给国内客户时几乎必踩,因为客户现场的文件路径大概率带中文。

6. 验证与提速:Top-1 对齐之后再做 INT8 量化的近路

6.1 性能基线怎么测

跑通不代表能交付,我习惯先测一个稳定的性能基线再谈优化。测法是用std::chrono::steady_clock包住Predict,对同一张图循环 100 次,跳过前 10 次预热,取后 90 次的 P95 和平均值。注意别在 Debug 配置下测,ORT 在 Debug 下性能会打折扣,Release +/O2才是真实水平。

我自己的经验数据:yolo11n-cls,224×224,4 线程,i5-8500 上 P95 大约在 15~25ms;换 1000 类输出时 softmax 那部分多出来的开销可以忽略。如果 P95 超过 50ms,先看线程数,再考虑降输入尺寸或换更小的权重。这种单模型场景用不到 cpp std execution 那套并行设施,线程数交给 ORT 内部调度更省心。

6.2 从 FP32 到 INT8:一条近路和它的检查清单

CPU 上最直接的提速手段是做 ONNX 动态量化,把权重从 float32 压到 int8,模型体积缩小到四分之一,推理常有 1.5~2 倍收益。做法极其简单:

# quantize_int8.py —— 动态量化,不需要校准数据 from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic("yolo11n-cls.onnx", "yolo11n-cls-int8.onnx", weight_type=QuantType.QInt8)

量化完成后,把它当普通模型替换进 C++ 工程,代码一行不用改。但有两个前提必须确认:一是量化前先跑 6.1 的基线,量化后跑同样的验证,Top-1 掉点超过 0.5% 就回退;二是如果你要同时对激活值做量化(降低内存带宽压力),需要准备几十张代表图做校准,动态量化不做校准,静态量化才需要,别混淆。

我自己的交付习惯是:工程目录里永远放一个 verify_baseline.py、一份 ref_output.npy、一份 labels.txt 和 FP32/INT8 两个模型文件。换模型、换量化、换输入尺寸,都先把参考输出重算一遍,再进 C++ 验收。这个习惯帮我挡掉了好几次「客户现场报错、回来一查是模型放错」的血泪教训。希望帮到你,把你的第一版 yolo11 分类部署尽快跑在客户那台旧电脑上。

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

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

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

立即咨询