ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++实战:基于YOLO与ONNX Runtime的动物识别系统开发

C++实战:基于YOLO与ONNX Runtime的动物识别系统开发 简介这是一份基于C的动物识别项目资源面向对计算机视觉和机器学习感兴趣的开发者内容覆盖图像预处理、特征提取、分类器训练与实时识别等完整环节。压缩包内共二十六个文件体积约十一点六兆包含源代码、工程配置、可执行程序与调试信息等文件类型便于直接编译运行和断点排查。项目借助OpenCV等库实现SIFT、HOG等特征描述与SVM分类对动物形状、纹理、颜色等特征进行匹配适合课程设计或入门实践参考。项目代码结构清晰注释简洁适合初学者逐步跟进。目前已有1276人学习解压后可以对照源码理解各功能模块的实现思路并根据实际需求调整参数或引入深度学习模型工程结构完整可快速跑通演示并做二次开发实操参考价值较高。1. 项目概述与核心价值1.1 动物识别项目到底在做什么用一个摄像头对准动物园的围栏或者对准田野里的监控画面程序自动告诉你画面里出现的动物是梅花鹿、藏羚羊还是东北虎这就是“动物识别”。这类项目在智慧养殖、自然保护区监测、动物园安全预警、野生动物的行为研究这些场景里特别常见本质上就是人工智能里说的“目标检测图像分类”任务。我在实际调研中发现一个很典型的需求高校的AI课程大作业、毕业设计或者刚刚入职的算法工程师接到的一个练手项目都特别喜欢拿动物识别来切入。原因很简单——数据集公开好搞像Oxford-IIIT Pet、COCO的子集都能直接下载类别的区分度也够有挑战性猫和狗还能靠纹理区分但狮子和老虎、羚羊和山羊这类外形接近的动物对模型的特征提取能力是实打实的考验。这个项目的技术栈选的是“人工智能C”这个组合在2025年看依然非常合理。Python在算法训练阶段几乎是统治级的但要谈到把模型部署到真实的监控盒子、嵌入式设备、边缘计算终端上C的高效性和可控性是Python很难替代的。我接手过几个实际部署项目训练好的YOLO模型在Python里跑推理帧率大概在15FPS迁移到C加上TensorRT加速能直接干到40FPS以上这个差距在实时监控场景里就是能不能用的区别。1.2 适合谁拿来学习和参考如果你是准备交人工智能大作业的学生这篇文章能帮你理清从模型选型到代码落地的完整链路如果你是做算法部署的工程师里面关于OpenCV DNN和ONNX Runtime的对比、多线程推理的优化方案可以直接抄作业就算你只是对C感兴趣的初学者也能从VSCode环境配置、CMake工程搭建、动态库链接这些基础操作里拿到一套可复用的实践模板。我把这个项目拆开来看核心涉及的知识点有这些目标检测模型YOLOv5/YOLOv8或SSD的结构与输出解析、C调用深度学习推理库、OpenCV的图像预处理与后处理NMS非极大值抑制、还有多线程并发处理视频流。后面我会按实际操作顺序把这些点全部串起来讲并且附上可以直接运行的代码和参数配置。2. 整体技术方案与模型选型2.1 模型选型不是越新越好适合场景最重要动物识别可以选的目标检测模型很多YOLO系列、SSD、EfficientDet、更轻量化的MobileNet-SSD都有。我的建议是如果跑在PC上YOLOv5s或者YOLOv8n最省心如果要部署到树莓派、Jetson Nano这样的边缘设备上就考虑MobileNetV4-SSD或者YOLOv5n模型文件小到几MB推理速度能保住。为什么我优先推荐YOLO系列而不是SSD最重要的一点是YOLO在同等计算量下对小目标的检测效果明显更好。动物识别场景里动物经常离摄像头很远在画面上只占几十个像素SSD的默认框对小目标的召回率不太够用。而YOLO从v5开始使用了自适应锚框计算和多尺度预测在COCO数据集上对中小目标的表现比SSD高出一截。模型选型还要考虑输出格式。初学者最容易忽略的是不同模型的输出张量结构完全不一样。SSD输出的是“1×N×(类别数5)”的二维张量其中5是坐标置信度YOLOv5输出的是三个不同尺度的特征图每个尺度对应“1×255×80×80”这种四维结构2553×(4180)。这个差异直接决定了后处理代码怎么写后文里我会详细讲。2.2 推理引擎选型OpenCV DNN还是ONNX Runtime模型训练好之后要导出成推理引擎能加载的格式。这里有两条路第一条路直接用OpenCV的dnn模块加载ONNX模型。OpenCV C接口里有cv::dnn::readNetFromONNX()加载之后用net.forward()就能完成推理。优点是不需要额外安装深度学习框架依赖只有一个OpenCV对新手极其友好缺点是OpenCV的DNN模块在部分算子上支持得不够好有些较新的YOLO版本导出ONNX之后会有兼容性问题需要改导出参数。第二条路用ONNX Runtime的C接口。这也是我后来在实际项目里主推的方案。ONNX Runtime对ONNX算子的支持覆盖率接近100%推理速度也比OpenCV DNN快5%~10%左右。它的C接口用起来也算直观核心就是Ort::Session这个类加载模型、创建输入输出张量、调用session.Run()。我做了一张对比表方便你根据自己的场景快速决定对比项OpenCV DNNONNX RuntimeTensorRT环境依赖仅OpenCVOpenCV ONNX RuntimeOpenCV CUDA TensorRT上手难度最低中等较高推理速度较慢较快最快需NVIDIA显卡算子兼容性一般好好适合场景课设/快速原型工业级部署高性能边缘计算如果你只是想先跑通流程、交一份大作业直接选OpenCV DNN省事如果是奔着实际部署去的建议一步到位用ONNX Runtime。我自己是从OpenCV DNN起步后来全部迁移到了ONNX Runtime下面讲代码的时候会涉及这两个方案的区别注意点。3. 环境搭建与工程配置3.1 VSCode CMake搭建C开发环境避坑指南现在做C开发VSCode配CMake是绝对的主流。我见过太多同学卡在环境配置这一步其实只要按下面顺序操作十几分钟就能搞定。首先安装编译器。Windows平台两个选择MSVCVisual Studio Build Tools或者MinGW-w64。我建议用MSVC因为后面编译OpenCV或者ONNX Runtime官方预编译库的时候它们默认就是MSVC编译的如果用MinGW去链MSVC编译出来的库会出现大量的符号不匹配错误。安装Visual Studio Build Tools时勾选“使用C的桌面开发”这一个工作负载就够。然后是VSCode插件必装的有四个C/C微软官方、CMake、CMake Tools、Code Runner。装完之后在项目根目录建一个CMakeLists.txt内容如下cmake_minimum_required(VERSION 3.20) project(AnimalDetector) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(OpenCV REQUIRED) find_package(onnxruntime REQUIRED) include_directories(${OpenCV_INCLUDE_DIRS} ${ONNXRUNTIME_INCLUDE_DIR}) add_executable(animal_detector src/main.cpp) target_link_libraries(animal_detector ${OpenCV_LIBS} ${ONNXRUNTIME_LIBRARIES})这里find_package(OpenCV REQUIRED)是依靠OpenCV安装时生成的OpenCVConfig.cmake文件来找路径的。用CMake工具直接点configure和buildVSCode会自动调用编译器完成编译。你如果只是临时跑个demo用命令行也行cd build cmake .. cmake --build . --config Release3.2 本地安装OpenCV和ONNX Runtime的版本匹配细节OpenCV的安装Windows下我强烈推荐直接从官网下载预编译的release版本不要自己从源码编译。自己编译OpenCV要折腾几个小时而预编译包解压即用。注意选版本时看准支持C17的另外把opencv_world4xxx.dll所在的bin目录加到系统PATH里。ONNX Runtime的安装同理直接到GitHub Release页面下载对应平台的预编译包。这里有个关键细节下载的包名里带gpu的就是CUDA版比如onnxruntime-win-x64-gpu-1.16.3需要你本地装了匹配版本的CUDA和cuDNN不带gpu的就是CPU版。选版本之前要确认你下载的ONNX模型是用哪个opset版本导出的ONNX Runtime版本不能低于模型要求的opset。环境配置完成后可以在VS Code里验证一下#include opencv2/opencv.hpp #include onnxruntime_cxx_api.h #include iostream int main() { std::cout OpenCV version: CV_VERSION std::endl; std::cout ONNX Runtime version: OrtGetApiBase()-GetVersionString() std::endl; return 0; }这串代码能正常输出版本号就说明环境没有问题。4. 核心代码实现从加载模型到输出结果4.1 图像预处理blobFromImage的每步操作拆开看模型加载完成之后第一步就是把读入的图像转成模型需要的输入张量。这里用的核心函数是OpenCV的cv::dnn::blobFromImage。#include opencv2/opencv.hpp #include opencv2/dnn.hpp #include vector #include string cv::Mat loadAndPreprocess(const std::string imagePath, int inputSize 640) { cv::Mat img cv::imread(imagePath); if (img.empty()) { throw std::runtime_error(Failed to load image: imagePath); } cv::Mat blob cv::dnn::blobFromImage(img, 1.0 / 255.0, // scale factor cv::Size(inputSize, inputSize), // 输入尺寸 cv::Scalar(0, 0, 0), // mean值 true, // 是否交换RB通道 false); // 是否裁剪 return blob; }这里几个参数背后都有讲究。scale factor设为1/255是因为模型训练时像素值被归一化到了0到1之间推理时输入也必须做同样的归一化否则模型性能会明显下降。cv::Size(640, 640)是根据YOLO模型的输入尺寸设置的你如果用YOLOv8官方模型输入就是640×640的RGB图这个时候交换RB通道为true是因为OpenCV默认读图顺序是BGR而模型训练的时候用的是RGB顺序不换过来颜色通道就反了。需要特别提醒如果使用ONNX Runtime接口不能直接用OpenCV的blob因为ONNX Runtime需要的是std::vectorfloat类型的连续内存数据。要从cv::Mat转过去可以这样操作std::vectorfloat matToVector(const cv::Mat mat) { // OpenCV的blob是四维NCHW格式这里直接拷贝数据到vector std::vectorfloat vec(mat.beginfloat(), mat.endfloat()); return vec; }我在实际项目里就是先blobFromImage生成cv::Mat再把它转成一个float数组喂给ONNX Runtime这样预处理代码可以共用。4.2 YOLO模型推理与后处理锚框到目标框的转换模型forward之后拿到的是原始输出张量要变成坐标框和类别信息必须做后处理。以YOLOv5导出ONNX为例输出形状通常是(1, 25200, 85)其中25200是三个尺度加起来的所有候选框数量85代表cx, cy, w, h, objectness, 80个类别得分。先写一个结构体存放检测结果struct Detection { cv::Rect box; float confidence; int classId; };核心的后处理代码分为三步置信度过滤、坐标解码、NMS抑制。我给出简洁版实现std::vectorDetection postProcess(const std::vectorfloat output, int inputWidth, int inputHeight, float confThreshold, float nmsThreshold) { const int numAnchors 25200; const int numClasses 80; std::vectorcv::Rect boxes; std::vectorfloat confidences; std::vectorint classIds; // 遍历所有锚框 for (int i 0; i numAnchors; i) { float confidence output[i * 85 4]; if (confidence confThreshold) continue; // 找出得分最高的类别 float maxClassScore 0.0f; int maxClassId -1; for (int j 5; j 85; j) { float score output[i * 85 j]; if (score maxClassScore) { maxClassScore score; maxClassId j - 5; } } float finalScore confidence * maxClassScore; if (finalScore confThreshold) continue; // 解码坐标模型输出是中心点坐标宽高需要还原到原图尺寸 float cx output[i * 85 0] / 640.0f * inputWidth; float cy output[i * 85 1] / 640.0f * inputHeight; float w output[i * 85 2] / 640.0f * inputWidth; float h output[i * 85 3] / 640.0f * inputHeight; int x static_castint(cx - w / 2); int y static_castint(cy - h / 2); boxes.push_back(cv::Rect(x, y, static_castint(w), static_castint(h))); confidences.push_back(finalScore); classIds.push_back(maxClassId); } // NMS非极大值抑制 std::vectorint indices; cv::dnn::NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, indices); std::vectorDetection detections; for (int idx : indices) { detections.push_back({boxes[idx], confidences[idx], classIds[idx]}); } return detections; }很多初学C的人第一次看到这段代码会觉得“这个坐标解码为什么是除以640再乘原图尺寸”。实际上模型训练和推理时图像被resize到了640×640所以模型预测的坐标值是相对于640×640这张缩放后图像的要还原到原图上就必须做一个比例映射。这是最容易忽略的细节之一。类别过滤也需要单独说YOLO输出中每个锚框会有一个objectness置信度和80个类别得分最终置信度计算方式在不同版本模型上不一样。YOLOv5是让objectness × 类别得分而YOLOv8及以后版本已经取消了objectness分支输出直接是(1, 84, 8400)结构其中84480。这个差别直接导致索引方式不同代码不能完全通用。4.3 完整推理流程与结果可视化把上面的函数串起来一个完整的C动物识别推理流程大概是这样的#include opencv2/opencv.hpp #include opencv2/dnn.hpp #include iostream #include fstream #include vector #include string int main(int argc, char** argv) { // 1. 加载类别名 std::vectorstd::string classNames; std::ifstream ifs(../models/coco.names); std::string line; while (std::getline(ifs, line)) { classNames.push_back(line); } // 2. 加载模型以ONNX Runtime为例 // 实际项目中需要按上一章说的安装ONNX Runtime并包含头文件 const char* modelPath ../models/yolov5s.onnx; // 3. 读取图像 cv::Mat img cv::imread(../test/animal.jpg); if (img.empty()) { std::cerr Failed to read image. std::endl; return -1; } // 4. 预处理推理后处理这里就不再重复贴前面已经写过的函数实现 cv::Mat blob cv::dnn::blobFromImage(img, 1.0 / 255.0, cv::Size(640, 640), cv::Scalar(0, 0, 0), true, false); // 5. 画框并输出结果 // 假设detections已经是后处理得到的检测结果 // for (const auto det : detections) { // cv::rectangle(img, det.box, cv::Scalar(0, 255, 0), 2); // std::string label classNames[det.classId] : // cv::format(%.2f, det.confidence); // cv::putText(img, label, cv::Point(det.box.x, det.box.y - 5), // cv::FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar(0, 255, 0), 2); // } cv::imshow(Animal Detection, img); cv::waitKey(0); return 0; }这里我特别说一下coco.names这个文件。它是一行一个类别名的纯文本文件用来把模型输出的类别编号映射成人类可读的名字。如果你用COCO预训练权重80类里包含猫、狗、马、羊、牛、大象、熊、斑马、长颈鹿等常见动物如果你想识别更有针对性的动物类别需要自己准备数据集并微调模型。4.4 从静态图到实时视频多线程与性能优化图片识别跑通之后自然要考虑摄像头实时视频流的识别。这里有个性能瓶颈视频流的解码、推理、显示如果都在单线程里串行执行帧率会非常难看。我建议用两个线程一个线程专门读取视频帧另一个线程做推理和绘制两个线程之间用队列传递帧数据用互斥锁保护队列。一个简化版的多线程框架如下std::mutex mtx; std::queuecv::Mat frameQueue; bool stopFlag false; void captureThread(cv::VideoCapture cap) { cv::Mat frame; while (!stopFlag) { cap frame; if (frame.empty()) break; std::lock_guardstd::mutex lock(mtx); if (frameQueue.size() 5) { frameQueue.push(frame.clone()); } } } void inferenceThread() { while (!stopFlag) { cv::Mat frame; { std::lock_guardstd::mutex lock(mtx); if (frameQueue.empty()) continue; frame frameQueue.front(); frameQueue.pop(); } // 在这里调用预处理、推理、后处理和绘制 // 如果推理平均耗时小于取帧间隔队列不会持续堆积 } }队列长度限制为5是防止摄像头帧率远高于推理速度时队列无限增长导致内存飙升。这种“生产者-消费者”模式在实际项目中非常常用比单纯调高OpenCV的set(CAP_PROP_BUFFERSIZE)要可靠得多。除了多线程推理性能上还有几个细节值得优化。OpenCV DNN可以在net.setPreferableBackend()里切换计算后端如果你有NVIDIA显卡设置为DNN_BACKEND_CUDA配合DNN_TARGET_CUDA能获得好几倍加速如果没有显卡CPU模式下把OpenCV编译时开启WITH_IPP也能提升部分性能。图像resize的时候用cv::INTER_LINEAR而不是默认插值方式速度会稍微快一点对精度几乎没有影响。5. 常见问题与排查技巧实录5.1 编译期错误与链接问题C项目最劝退新人的就是编译报错。前前后后我帮别人排查过不少次这里把最典型的问题列出来。无法打开文件opencv_world4xx.lib这类问题多半是CMake的find_package没找到库路径。在CMakeLists里显式设置OpenCV_DIR指向OpenCV安装目录下的build文件夹基本上就能解决。LNK2019: 无法解析的外部符号这个通常是链接阶段少了库。ONNX Runtime用的时候除了链接onnxruntime.lib可能还需要把onnxruntime.dll拷贝到可执行文件所在的目录。Visual Studio下Debug模式和Release模式链接的库版本如果不是同一个也容易出现符号不一致问题尽量保持配置统一。最隐蔽的是OpenCV的Debug版和Release版混用。Debug版库名带d后缀比如opencv_world4xxd.lib如果你在Debug模式下链接了Release版库程序运行时会出现诡异的崩溃。CMake里find_package(OpenCV REQUIRED)回填的OpenCV_LIBS一般会自动匹配当前编译模式但手写路径时就要格外注意。5.2 推理结果不对或精度明显下降如果你发现识别的动物类别总是错或者框的位置明显偏了先别怀疑模型有问题按下面的顺序排查先检查输入图片的通道顺序。OpenCV读进来是BGR训练时用的是RGB忘了交换通道的话猫看起来可能像狗——因为颜色特征完全错乱了。再检查resize的方式。如果模型输入要求640×640而你是把图片直接压扁成640×640长宽比变化会导致目标物体变形检测精度大打折扣。正确做法是用cv::resize保持长宽比不足的部分用灰色填充。YOLO官方仓库给出的letterbox操作就是这个原理。最后检查归一化的尺度。YOLO系列一般是1/255归一化但有些模型用的是0~1以外的标准化方式比如ImageNet的mean/std标准化。如果用了错误的归一化参数模型输出的置信度会普遍偏低出现“什么都检测不到”或者“检测框置信度全都很低”的现象。5.3 帧率上不去的瓶颈定位多线程做完了帧率还是很低问题往往出在推理引擎的后端选择上。CPU模式下YOLOv5s在普通PC上推理一张640×640的图大约需要80到150毫秒这已经算正常水平如果你发现耗时超过300毫秒优先检查是不是OpenCV DNN把模型跑在了CPU的默认后端上而没有启用IPP或者VCL优化。还有一个容易踩的坑OpenCV的imshow显示窗口默认有垂直同步会拖慢主循环。不要每帧都调用imshow可以做成每3帧才刷新一次画面或者直接把waitKey(1)改为waitKey(30)实测下来帧率体感会顺滑很多。实时视频识别本来也不要求每一帧都画框推理和显存会自己隔断合理抽帧反而能减少CPU占用。6. 写在最后一点实在的建议我自己做过好几个CV部署项目最大的感受是模型训练再花哨最后还是要落到工程代码里。用C做动物识别这个项目技术上谈不上多前沿但它能把“训练好的模型怎么真正跑起来”这件事练得明明白白。如果你正在拿这个题目做大作业我的建议是先专注把OpenCV DNN这条链路跑通再考虑换ONNX Runtime、加多线程、上TensorRT这些进阶优化——一步一步来每个环节踩过的坑都是实打实的经验。项目中如果你卡在某一步多看看模型导出的opset版本、OpenCV版本、编译器版本这三者的匹配关系大概率能找到原因。祝顺利跑通你的第一个C动物识别程序。本文还有配套的精品资源点击获取
返回列表