
简介目标检测是计算机视觉的核心任务之一其原理是通过深度学习模型识别图像中的物体并定位其位置。YOLO系列模型因其在精度与速度间的平衡而广受欢迎而模型部署则是将训练好的模型应用于实际场景的关键环节。在资源受限的边缘设备或低配置开发环境中高效的部署方案能显著提升技术价值。ONNXRuntime作为一个跨平台的高性能推理引擎支持ONNX模型格式为C等高性能语言提供了便捷的部署接口。结合OpenCV进行图像处理可以在Ubuntu等系统上构建完整的推理流水线。本文聚焦于YOLOv8模型的C推理部署详细解析了从环境搭建、模型预处理、onnxruntime引擎调用到后处理解析的全过程旨在为低配硬件环境下的深度学习应用提供一套可行的工程实践方案。1. 项目缘起为什么要在低配机器上折腾YOLOv8推理最近在社区里看到不少朋友对YOLOv8这类目标检测模型很感兴趣但一提到训练和部署第一反应就是需要一张RTX 4090或者至少是30系显卡。这其实是一个很大的误解。训练模型确实需要强大的算力但模型的推理Inference——也就是用训练好的模型去识别图片或视频——对硬件的要求要友好得多。我手头就有一台老旧的笔记本CPU是i5-8250U显卡是早已过时的MX150内存也只有8GB。用这台机器去训练YOLOv8无疑是痴人说梦但用它来跑通一个训练好的YOLOv8模型进行实时检测不仅可行而且体验还不错。这就是我做这个项目的初衷打破“深度学习必须高配置”的迷思提供一个在Ubuntu系统下用最基础的C工具链结合onnxruntime和OpenCV从零开始实现YOLOv8模型推理、解析和部署的完整方案。整套代码和文档都是开源的目标就是让你能在自己的低配电脑上实实在在地体验一把深度学习的应用理解从模型文件到检测结果的全过程而不是仅仅停留在调用Python接口的层面。选择C而不是Python核心原因在于部署效率和控制力。Python虽然方便但在生产环境或资源受限的边缘设备上其解释器开销和动态特性可能成为性能瓶颈。C能提供更接近硬件的性能内存管理也更精细这对于我们榨干低配机器的每一分算力至关重要。而onnxruntime作为一个高性能推理引擎对ONNX模型格式的支持非常成熟跨平台特性也好完美契合我们的需求。2. 环境准备打造一个纯净高效的C开发工作站在Ubuntu上搞C开发第一步就是把环境搭建扎实。很多人觉得Linux配置环境麻烦其实步骤清晰后反而比Windows下各种路径问题要清爽。2.1 系统与编译器我使用的是Ubuntu 22.04 LTS长期支持版比较稳定。首先更新系统并安装必要的编译工具sudo apt update sudo apt upgrade -y sudo apt install build-essential cmake git pkg-config -ybuild-essential包含了GCC、G、make等核心编译工具。CMake是我们后续管理项目依赖和编译过程的关键必须安装。2.2 核心依赖库OpenCV与onnxruntime我们的项目两大支柱就是OpenCV处理图像和onnxruntime运行模型。1. 安装OpenCVOpenCV的安装方法很多从apt仓库安装最简单但版本可能较旧。为了获得更好的性能和兼容性我推荐从源码编译安装。这里以安装OpenCV 4.8.0为例# 1. 安装OpenCV的依赖项 sudo apt install libjpeg-dev libpng-dev libtiff-dev libavcodec-dev libavformat-dev libswscale-dev libv4l-dev libxvidcore-dev libx264-dev libgtk-3-dev libatlas-base-dev gfortran libopenblas-dev liblapack-dev -y # 2. 下载OpenCV源码包含opencv_contrib提供更多模块 cd ~ git clone https://github.com/opencv/opencv.git git clone https://github.com/opencv/opencv_contrib.git cd opencv git checkout 4.8.0 cd ../opencv_contrib git checkout 4.8.0 # 3. 创建构建目录并配置 cd ~/opencv mkdir build cd build cmake -D CMAKE_BUILD_TYPERELEASE \ -D CMAKE_INSTALL_PREFIX/usr/local \ -D OPENCV_EXTRA_MODULES_PATH~/opencv_contrib/modules \ -D WITH_CUDAOFF \ # 低配机器通常无CUDA关闭以简化编译 -D BUILD_EXAMPLESOFF \ -D BUILD_opencv_python3OFF \ # 我们不需Python绑定 -D BUILD_TESTSOFF \ -D BUILD_PERF_TESTSOFF .. # 4. 编译并安装-j$(nproc) 使用所有CPU核心加速编译但低配机器建议用 -j2 或 -j4 防止卡死 make -j4 sudo make install sudo ldconfig # 更新动态链接库缓存注意编译OpenCV是个耗时较长的过程在我的低配笔记本上花了近一个小时。期间风扇狂转是正常的。关键是-D WITH_CUDAOFF因为我们没有NVIDIA GPU或者显卡太老不支持CUDA开启反而会报错。2. 安装onnxruntimeonnxruntime提供了预编译的C库我们直接下载使用省去编译时间。cd ~ # 前往GitHub Release页面查看最新版本例如1.16.3 wget https://github.com/microsoft/onnxruntime/releases/download/v1.16.3/onnxruntime-linux-x64-1.16.3.tgz tar -xzvf onnxruntime-linux-x64-1.16.3.tgz sudo cp -r onnxruntime-linux-x64-1.16.3/include/* /usr/local/include/ sudo cp onnxruntime-linux-x64-1.16.3/lib/libonnxruntime.so* /usr/local/lib/ sudo ldconfig这里我们只拷贝了头文件和动态链接库.so文件。静态库.a通常更大动态链接更适合我们这种项目。2.3 开发工具与项目初始化我习惯用VSCode进行开发它轻量且插件丰富。通过Snap安装即可sudo snap install code --classic在VSCode中安装C/C、CMake Tools插件。然后为我们的项目创建一个清晰的工作目录mkdir ~/yolov8_cpp_inference cd ~/yolov8_cpp_inference mkdir build src include bin modelssrc/: 存放我们的.cpp源文件。include/: 存放头文件.h或.hpp。build/: CMake的构建目录所有中间文件都在这里生成保持源码目录清洁。bin/: 存放最终生成的可执行文件。models/: 存放YOLOv8的ONNX模型文件。接下来在项目根目录创建CMakeLists.txt文件这是项目的“总说明书”。3. 模型准备与预处理理解YOLOv8 ONNX模型的输入输出在写代码之前我们必须搞清楚我们要喂给模型什么以及模型会吐出什么。3.1 获取YOLOv8 ONNX模型如果你已经用Ultralytics的YOLOv8训练了自己的模型导出ONNX格式非常简单from ultralytics import YOLO model YOLO(path/to/your/best.pt) # 加载训练好的权重 model.export(formatonnx) # 导出ONNX模型如果没有可以直接使用官方预训练的模型例如YOLOv8n纳米级最小最快cd ~/yolov8_cpp_inference/models wget https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n.onnx将下载好的yolov8n.onnx放在models/目录下。3.2 剖析模型接口使用Netron可视化用netron工具一个Python包打开这个ONNX文件是理解模型结构最直观的方式。pip install netron python -m netron models/yolov8n.onnx在打开的网页中我们可以看到输入 (Input): 一个名为images的节点其形状通常是[1, 3, 640, 640]。这代表批大小(Batch Size)为13个颜色通道RGB图片高度和宽度均为640像素。这意味着模型要求输入图片必须被缩放Resize到640x640并且像素值需要从0-255的整数归一化(Normalize)到0-1的浮点数。输出 (Output): 一个名为output0的节点其形状是[1, 84, 8400]。这是YOLOv8输出的关键需要重点解析1: 批大小。84: 每个预测框的属性数量。对于COCO数据集80个类别其构成为[cx, cy, w, h, confidence, class_prob_1, class_prob_2, ..., class_prob_80]。即4个坐标偏移量中心点x,y宽度w高度h1个框的置信度以及80个类别的概率。8400: 模型在640x640的网格上生成的预测框总数。YOLOv8使用了多尺度特征图例如80x80, 40x40, 20x20进行预测8400就是这个总数。3.3 设计预处理函数根据以上分析我们的预处理函数在C中实现需要完成以下步骤读取图像使用OpenCV的cv::imread。保持宽高比缩放直接将图片cv::resize到640x640会严重变形。正确做法是计算缩放比例将长边缩放到640短边按比例缩放然后在短边两侧进行填充Padding使最终图像为640x640的正方形。填充的颜色通常用灰色(114, 114, 114)。图像数据转换OpenCV默认读取的图像数据是HWC格式高度、宽度、通道且通道顺序是BGR。我们需要将其转换为CHW格式并且转换为RGB顺序。同时将uint8类型的像素值0-255除以255.0转换为float32类型0.0-1.0。构造输入张量将处理好的float32数组传递给onnxruntime构造一个形状为[1, 3, 640, 640]的Ort::Value张量。这个预处理过程是保证推理正确的基石任何一步出错都会导致检测结果异常甚至崩溃。4. 核心推理引擎搭建用onnxruntime加载并运行模型环境好了模型懂了现在进入核心环节——用C代码把模型跑起来。4.1 编写CMakeLists.txt首先完善项目根目录的CMakeLists.txt它告诉CMake如何找到依赖库并编译我们的项目。cmake_minimum_required(VERSION 3.16) project(YOLOv8_CPP_Inference) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 设置可执行文件输出目录 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin) # 查找OpenCV find_package(OpenCV REQUIRED) include_directories(${OpenCV_INCLUDE_DIRS}) # 查找onnxruntime (我们手动指定路径) set(ONNXRUNTIME_ROOT_DIR /usr/local) find_path(ONNXRUNTIME_INCLUDE_DIR onnxruntime_c_api.h PATHS ${ONNXRUNTIME_ROOT_DIR}/include NO_DEFAULT_PATH) find_library(ONNXRUNTIME_LIB onnxruntime PATHS ${ONNXRUNTIME_ROOT_DIR}/lib NO_DEFAULT_PATH) if (ONNXRUNTIME_INCLUDE_DIR AND ONNXRUNTIME_LIB) message(STATUS Found onnxruntime: ${ONNXRUNTIME_LIB}) include_directories(${ONNXRUNTIME_INCLUDE_DIR}) else() message(FATAL_ERROR Cannot find onnxruntime library, please check your installation.) endif() # 添加可执行文件 add_executable(yolov8_inference src/main.cpp src/preprocess.cpp src/postprocess.cpp src/utils.cpp ) # 链接库 target_link_libraries(yolov8_inference ${OpenCV_LIBS} ${ONNXRUNTIME_LIB} )4.2 创建推理管理器类为了代码清晰我创建了一个InferenceEngine类来封装onnxruntime的相关操作。头文件include/inference_engine.hpp如下#ifndef INFERENCE_ENGINE_HPP #define INFERENCE_ENGINE_HPP #include onnxruntime_cxx_api.h #include opencv2/opencv.hpp #include vector #include string class InferenceEngine { public: InferenceEngine(const std::string model_path); ~InferenceEngine(); // 预处理并推理单张图片 std::vectorOrt::Value infer(const cv::Mat image); // 获取模型输入输出信息 std::vectorint64_t get_input_shape() const; std::vectorint64_t get_output_shape() const; std::string get_input_name() const; std::string get_output_name() const; private: Ort::Env env_; Ort::SessionOptions session_options_; Ort::Session session_{nullptr}; Ort::AllocatorWithDefaultOptions allocator_; std::vectorconst char* input_names_; std::vectorconst char* output_names_; std::vectorint64_t input_shape_; std::vectorint64_t output_shape_; void init_session(const std::string model_path); }; #endif // INFERENCE_ENGINE_HPP对应的源文件src/inference_engine.cpp重点在于初始化会话和运行推理#include inference_engine.hpp #include iostream InferenceEngine::InferenceEngine(const std::string model_path) { // 初始化环境日志级别设为警告减少输出 env_ Ort::Env(ORT_LOGGING_LEVEL_WARNING, YOLOv8Inference); // 设置会话选项对于CPU推理使用默认选项即可 session_options_.SetIntraOpNumThreads(1); // 设置线程数低配机器设为1更稳定 session_options_.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); init_session(model_path); } void InferenceEngine::init_session(const std::string model_path) { try { // 加载ONNX模型 session_ Ort::Session(env_, model_path.c_str(), session_options_); // 获取模型输入输出信息 Ort::AllocatedStringPtr input_name_ptr session_.GetInputNameAllocated(0, allocator_); Ort::AllocatedStringPtr output_name_ptr session_.GetOutputNameAllocated(0, allocator_); input_names_ {input_name_ptr.get()}; output_names_ {output_name_ptr.get()}; // 获取输入输出形状 Ort::TypeInfo input_type_info session_.GetInputTypeInfo(0); Ort::TensorTypeAndShapeInfo input_tensor_info input_type_info.GetTensorTypeAndShapeInfo(); input_shape_ input_tensor_info.GetShape(); Ort::TypeInfo output_type_info session_.GetOutputTypeInfo(0); Ort::TensorTypeAndShapeInfo output_tensor_info output_type_info.GetTensorTypeAndShapeInfo(); output_shape_ output_tensor_info.GetShape(); std::cout Model loaded successfully. std::endl; std::cout Input name: input_names_[0] , shape: ; for (auto dim : input_shape_) std::cout dim ; std::cout std::endl; std::cout Output name: output_names_[0] , shape: ; for (auto dim : output_shape_) std::cout dim ; std::cout std::endl; } catch (const Ort::Exception e) { std::cerr ONNXRuntime error: e.what() std::endl; throw; } } std::vectorOrt::Value InferenceEngine::infer(const cv::Mat image) { // 预处理步骤下一节详述会在这里被调用将cv::Mat转换为符合输入形状的float数组 // 假设preprocess_image函数返回一个std::vectorfloat data和一个cv::Size original_size cv::Mat processed_blob; cv::Size original_size; // ... 调用预处理函数填充 processed_blob 和 original_size ... // 这里先省略重点展示onnxruntime的推理调用 // 创建输入Tensor size_t input_tensor_size 1 * 3 * 640 * 640; // 根据input_shape_计算 std::vectorfloat input_tensor_values(input_tensor_size); // ... 将processed_blob中的数据拷贝到input_tensor_values ... Ort::MemoryInfo memory_info Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); std::vectorOrt::Value input_tensors; input_tensors.emplace_back(Ort::Value::CreateTensorfloat( memory_info, input_tensor_values.data(), input_tensor_size, input_shape_.data(), input_shape_.size() )); // 运行推理 std::vectorOrt::Value output_tensors session_.Run( Ort::RunOptions{nullptr}, input_names_.data(), input_tensors.data(), input_tensors.size(), output_names_.data(), output_names_.size() ); return output_tensors; } // ... 其他成员函数实现 ...这个类封装了onnxruntime的核心操作使得在主程序中我们只需要几行代码就能完成推理InferenceEngine engine(models/yolov8n.onnx); cv::Mat image cv::imread(test.jpg); auto outputs engine.infer(image); // 接下来对outputs进行后处理5. 后处理解析从8400个预测框中筛选出最终结果模型推理的输出是一个形状为[1, 84, 8400]的张量。这8400个框里绝大部分都是背景或者重复的预测我们需要通过后处理来筛选出最终有效的检测框。这是整个流程中最考验算法理解的部分。5.1 解码预测框首先我们需要将模型输出的[cx, cy, w, h]相对于特征图网格的偏移量转换回原始图像上的绝对坐标[x1, y1, x2, y2]框的左上角和右下角坐标。但是注意我们的输入图像是经过填充的640x640正方形而原始图像可能是任意长宽比。因此转换后的坐标还需要根据我们预处理时的缩放和填充信息映射回原始图像的尺寸。假设预处理时我们将原始图像按比例缩放并在两侧填充了灰色区域。我们需要记录下scale缩放比例scale min(640 / img_width, 640 / img_height)。pad_w,pad_h在宽度和高度方向上的填充像素数。那么对于模型输出的一个框(cx_out, cy_out, w_out, h_out)其在**填充后图像(640x640)**上的坐标计算如下YOLO公式x1 (cx_out - w_out / 2) * 640 y1 (cy_out - h_out / 2) * 640 x2 (cx_out w_out / 2) * 640 y2 (cy_out h_out / 2) * 640然后需要去除填充映射回原始图像尺寸x1 (x1 - pad_w) / scale y1 (y1 - pad_h) / scale x2 (x2 - pad_w) / scale y2 (y2 - pad_h) / scale最后确保坐标不超出原始图像边界。5.2 置信度过滤与非极大值抑制NMS解码后我们得到了8400个框在原始图像上的坐标、每个框的置信度conf以及80个类别的概率cls_probs。计算每个框的最终得分和类别对于每个框找到80个类别概率中最大的那个值max_cls_prob及其对应的类别IDclass_id。该框的最终得分score conf * max_cls_prob。置信度阈值过滤设定一个阈值如conf_threshold 0.25丢弃所有score conf_threshold的框。这一步可以过滤掉大部分背景框。非极大值抑制NMS经过上一步可能还剩下几十甚至上百个框其中很多框是重叠的检测的是同一个物体。NMS的目的就是去除这些冗余框。其核心思想是将所有框按score从高到低排序。选取分数最高的框A将其加入最终结果列表。计算框A与剩余所有框的IoU交并比即两个框重叠面积除以并集面积。丢弃所有与框A的IoU大于某个阈值如nms_threshold 0.45的框因为它们很可能和A检测的是同一个物体。从剩余的框中再选取分数最高的重复上述过程直到没有框剩下。5.3 C代码实现在src/postprocess.cpp中我们实现上述逻辑#include postprocess.hpp #include algorithm #include cmath struct Detection { cv::Rect bbox; // 矩形框 float score; // 置信度得分 int class_id; // 类别ID }; float iou(const cv::Rect a, const cv::Rect b) { float inter_area (a b).area(); float union_area a.area() b.area() - inter_area; return inter_area / union_area; } std::vectorDetection postprocess( const std::vectorfloat output_data, // 模型输出的扁平化数据 [1,84,8400] - 1*84*8400个float const cv::Size original_size, const cv::Size padded_size, const std::pairfloat, float scale, // scale_x, scale_y const std::pairint, int pad, // pad_left, pad_top float conf_threshold 0.25f, float iou_threshold 0.45f) { std::vectorDetection detections; int num_classes 80; // COCO数据集 int num_boxes output_data.size() / (num_classes 4 1); // 8400 for (int i 0; i num_boxes; i) { const float* ptr output_data.data() i * (num_classes 5); // 每个框85个值 // 解析坐标和置信度 float cx ptr[0]; float cy ptr[1]; float w ptr[2]; float h ptr[3]; float box_conf ptr[4]; // 框置信度 // 找到最大类别概率 int class_id -1; float max_cls_prob 0.0f; for (int c 0; c num_classes; c) { float cls_prob ptr[5 c]; if (cls_prob max_cls_prob) { max_cls_prob cls_prob; class_id c; } } float score box_conf * max_cls_prob; if (score conf_threshold || class_id -1) { continue; // 过滤低置信度框 } // 解码坐标到填充后图像尺寸 float x1 (cx - w / 2.0f) * padded_size.width; float y1 (cy - h / 2.0f) * padded_size.height; float x2 (cx w / 2.0f) * padded_size.width; float y2 (cy h / 2.0f) * padded_size.height; // 去除填充映射回原始图像尺寸 x1 (x1 - pad.first) / scale.first; y1 (y1 - pad.second) / scale.second; x2 (x2 - pad.first) / scale.first; y2 (y2 - pad.second) / scale.second; // 确保坐标在图像范围内 x1 std::max(0.0f, std::min(x1, (float)original_size.width)); y1 std::max(0.0f, std::min(y1, (float)original_size.height)); x2 std::max(0.0f, std::min(x2, (float)original_size.width)); y2 std::max(0.0f, std::min(y2, (float)original_size.height)); if (x2 x1 || y2 y1) { continue; // 无效框 } detections.push_back({ cv::Rect(cv::Point((int)x1, (int)y1), cv::Point((int)x2, (int)y2)), score, class_id }); } // NMS std::vectorDetection final_detections; std::sort(detections.begin(), detections.end(), [](const Detection a, const Detection b) { return a.score b.score; }); while (!detections.empty()) { final_detections.push_back(detections[0]); auto last final_detections.back(); detections.erase(std::remove_if(detections.begin() 1, detections.end(), [](const Detection det) { return iou(last.bbox, det.bbox) iou_threshold; }), detections.end()); detections.erase(detections.begin()); // 移除当前最高分框 } return final_detections; }6. 性能优化与实测踩坑在低配机器上跑出可用帧率代码写完了能跑通只是第一步。在低配机器上我们必须关注性能。我的MX150笔记本CPU性能孱弱内存也小不加优化的话一帧图片处理可能要几百毫秒完全无法实时。6.1 预处理与后处理的优化避免不必要的拷贝在预处理函数中将BGR转RGB、HWC转CHW、归一化这几个步骤尽量在一次循环中完成直接填充到目标float数组中避免中间创建多个cv::Mat对象。使用指针操作在遍历图像像素进行格式转换时使用cv::Mat::ptrT()获取行指针比使用cv::Mat::atT()逐个访问要快得多。后处理中的计算简化在NMS中IoU计算是瓶颈。可以优先使用cv::Rect的求交和|求并运算符它们内部是优化过的。此外在排序前可以先进行一次粗略的面积过滤面积太小的框可以直接丢弃。6.2 onnxruntime会话配置在创建Ort::Session时我们做了一些设置session_options_.SetIntraOpNumThreads(1);对于低配CPU有时设置多线程反而会因为线程切换开销导致性能下降。实测在我的4核8线程i5上设为1或2效果最好。这需要根据具体CPU进行测试。session_options_.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);开启所有图优化这是必须的能显著提升推理速度。6.3 实测数据与瓶颈分析在我的设备上i5-8250U, 8GB RAM处理一张640x640的图片各阶段耗时大致如下使用std::chrono测量图像读取与解码~5ms (取决于图片大小和磁盘速度)。预处理缩放、填充、格式转换~15ms。这是可以优化的重点如果使用GPUOpenCV CUDA模块会快很多但我们的前提是低配。onnxruntime推理~120ms。这是最大的瓶颈取决于模型复杂度和CPU算力。YOLOv8n是最小的如果是YOLOv8s或更大时间会成倍增加。后处理解码NMS~10ms。对于8400个框这个速度可以接受。总耗时约150ms即每秒约6-7帧FPS。对于实时视频来说这个帧率勉强可以接受但会有明显卡顿。如果换成更轻量的模型如NanoDet、YOLOv5n或者进行模型量化将FP32模型转换为INT8帧率可以提升到15-20 FPS达到基本流畅的水平。踩坑记录最初我没有记录预处理时的缩放和填充信息导致后处理映射回原始坐标时全部错位检测框要么飘在图像外要么缩在中心一个小点。务必在预处理函数中返回scale和pad信息并在后处理中严格使用。7. 完整流程串联与效果展示将以上所有模块串联起来我们的src/main.cpp主函数就清晰了#include inference_engine.hpp #include preprocess.hpp #include postprocess.hpp #include opencv2/opencv.hpp #include iostream int main() { // 1. 初始化推理引擎 InferenceEngine engine(../models/yolov8n.onnx); // 2. 读取图像 cv::Mat image cv::imread(../images/test.jpg); if (image.empty()) { std::cerr Could not read the image. std::endl; return -1; } // 3. 预处理 cv::Mat blob; cv::Size original_size image.size(); cv::Size padded_size(640, 640); std::pairfloat, float scale; // scale_x, scale_y std::pairint, int pad; // pad_left, pad_top // 假设preprocess函数完成缩放、填充、BGR2RGB、HWC2CHW、归一化并返回blob和变换参数 preprocess(image, blob, scale, pad); // 4. 推理 (这里需要将blob数据传递给engine.infer略去细节) // 假设我们有一个包装函数 run_inference它内部调用engine.infer并返回原始输出数据 std::vectorfloat output_data run_inference(engine, blob); // 5. 后处理 std::vectorDetection detections postprocess(output_data, original_size, padded_size, scale, pad); // 6. 可视化结果 cv::Mat result_image image.clone(); for (const auto det : detections) { cv::rectangle(result_image, det.bbox, cv::Scalar(0, 255, 0), 2); std::string label Class std::to_string(det.class_id) : std::to_string(det.score); cv::putText(result_image, label, cv::Point(det.bbox.x, det.bbox.y - 5), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 255, 0), 1); } cv::imshow(Detection Result, result_image); cv::waitKey(0); return 0; }编译并运行cd ~/yolov8_cpp_inference/build cmake .. make -j4 ./bin/yolov8_inference如果一切顺利你将看到打开的图片上画出了绿色的检测框和类别标签。至此一个完整的、从模型到可视化结果的YOLOv8 C推理流程就全部跑通了。这个项目的价值不在于实现了一个多炫酷的功能而在于提供了一条清晰的路径证明了在有限的硬件条件下深入理解并实践深度学习模型部署的每一个环节是完全可行的。它剥离了高级框架的封装让你亲手触碰数据流动的每一个字节理解从图像张量到检测框的完整变换。这对于想深入AI部署领域或需要在资源受限环境下应用AI的开发者来说是一次非常扎实的练手。本文还有配套的精品资源点击获取