Win11系统下Yolov8的C++编译环境实践

Date:2024年1月24日

概要说明

yolo简介及背景

        yolo(You Only Look Once)是目前一种普及性较高的人工智能目标检测方法,它将目标检测任务重新定义为单一的回归问题,从而实现了从图像像素到边界框坐标和类别概率的直接映射。在业界,Yolo的新应用层出不穷,其需求多样变化、源码结构复杂化、跨编程语言不成熟等问题依然存在,处于一个快速更迭的阶段。

开发需求

        本文的背景是需要在Windows 11(x64) 环境下,以时下最新,也最具检测能力的 yolov8 框架上,实现对OpenCV、Python和C++集成开发环境的部署。由于没有发现网上有较为详细的搭建指南,笔者根据工程需要进行整理,并提供过程中出现的部分错误与对应的解决方法。为了长期支持的考虑,本文采用的各个模块均为2年以内的稳定版本。

        阅读本文可以帮助读者完成yolov8基础编译环境的快速部署,以满足 yolov8 在C++工业应用上的工程化需要。

        本文所用到的环境版本如下:

        cmake 3.27.1

        libtorch 2.1.2 + cpu

        opencv 4.8.1

        vs 2017

正文

一、搭建Windows下python环境

小节概述

由于yolo默认支持于Windows/Linux环境下的python语言,且大部分算法实施与调优依靠此环境,因此给出目前通用的python下开发环境搭建过程。如仅需要C++的开发环境配置,请直接阅读本文正文部分的第二节:搭建Windows下C++环境.

本小节需要的素材有:

  •         Anaconda
  •         Pytorch
  •         Yolov8

搭建过程

1.Anaconda环境

        Anaconda是一个Python包管理软件,利用工具/命令conda来进行package和environment的管理,可以很方便地解决多版本Python并存、切换以及各种第三方包安装问题。在我们激活yolo进行训练任务前,对conda环境的配置是较为广泛易用的方法。

        我们选择 Anaconda/miniconda 两个版本中的一个进行下载。为了更好的兼容CUDA,建议选择3.8(也即后缀-py38_以上的版本,无需使用最新版),对于比较基础的功能使用,笔者推荐选择包体较小的 miniconda 即可。

        Anaconda:

https://www.anaconda.com/

        miniconda:

Index of /anaconda/miniconda/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror

        二者的下载均在清华大学公开源中下载即可,无需使用vpn,下面是以miniconda为例进行的安装操作,如需使用完整的Anaconda也可以同时操作。

        安装过程中,请点选使用conda的PATH环境变量支持

        安装完成后,在联网环境下创建一个新的虚拟环境,测试是否可以用conda分别管理多个项目的包支持。

1、打开Anaconda prompt

        PyPI镜像设置国内默认源,此操作可以将软件所需依赖转到国内下载

        输入命令:(参考pypi | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror

        pip config set global.index-url Simple Index

2、创建与激活环境【注意很多操作要在激活环境下完成】

        创建新的conda环境的命令:

        conda create -n yolov8test python=3.8

        其中 -n yolov8test 是笔者自定义的conda空间名,python=3.8是指定python的版本为v3.8版本

        回车,输入y,完成所需的安装,显示:Executing transaction: done 即成功

        激活conda环境的命令:

        conda activate yolov8test

2.安装pytorch

        在GPU工作环境下,安装pytorch需要 CUDA 的支持。如果只需要训练、简单推理,则无需单独安装CUDA,直接安装pytorch;如果有部署需求,例如导出TensorRT模型,则需要进行CUDA安装。由于显卡的CUDA版本各有不同,因此我们需要根据自己的 Cuda 版本,选择不同的 pytorch 版本

        1、查询本机显卡对应的 CUDA 版本

        打开cmd窗口,以 Nvidia Geforce 3060为例,

        输入命令,在图示位置找到 CUDA 版本信息:

        nvidia-smi

        2、下载Pytorch,选择对应的版本进行安装即可

PyTorch

需要注意:

1)安装CUDA不是必须项

2) 16XX 系列的Nvidia显卡,请固定安装cu102的版本,否则可能训练出现问题。

        30XX、40XX系列的Nvidia显卡,要安装【cu111以上】的版本,否则无法正常运行

3、激活conda环境,在Anaconda prompt下输入指令:

        conda activate yolov8test

        (再次提醒,“yolov8test”是个人自定义的conda空间名称,此处请使用你自己的conda空间)

4、进行指定版本的pytorch和cuda工具链的安装

        笔者在这里使用了 v1.13.0 版本安装未成功,推荐使用 v1.12.0 及以下版本。

        输入指令:

        conda install pytorch==1.12.0 torchvision==0.13.0 torchaudio==0.12.0 cudatoolkit=11.3 -c pytorch

        出现 Proceed ([y]/n)? 信息,输入y

        等一阵子以后,会有一个进度条卡在(more hidden),稍等一下即可完成安装

3.源码安装Yolov8

        首先获取yolov8源码,从github上进行clone:

GitHub - ultralytics/ultralytics: NEW - YOLOv8 🚀 in PyTorch > ONNX > OpenVINO > CoreML > TFLite

        下载完成后,

        打开cmd,通过以下命令进入到 ultralytics-main 源码目录下(请注意,路径请选用自己实际的yolo源码放置路径)

        cd /d D:\3_yolov8\01_envPrepare\ultralytics-main\ultralytics-main

        安装源码:

        pip install -e .

        安装成功后,对该代码进行检查

        pip list

        如果在列表中看到 ultralytics 则成功

二、搭建Windows下C++环境()

        yolo默认支持于Linux环境下的python语言,其x86环境下的C++工具链需要手动配置。本文以Visual Studio 2017为开发C++程序的IDE进行配置,如需更高的Visual Studio可以自行切换

        本小节需要的素材有:

  •         Cmake
  •         OpenCV(若无特别需求,务必使用 OpenCV 4.8.1版本)
  •         Visual Studio
  •         Yolov8
1.安装 Cmake

        下载Cmake,由于外网限制,这里直接给出国内源镜像版本

Index of /files/PreviousRelease

        笔者选择 cmake-3.27.1-windows-i386.msi 这个版本,进行可执行程序安装、

        安装完成后,进入Windows系统环境变量设置对话框,如下图所示。可以看到,由于刚才的设置CMake已经自动将其安装路径“D:\Program Files\CMake\bin” 写入环境变量Path中。

2.安装 Libtorch

        Libtorch实际上是pytorch的C++支持库,这个直接在 pytorch 的官网 PyTorch 下载即可。

        建议选择 debug 版本

3.安装 OpenCV 4.8.1

        OpenCV的源码url在Github,这里给出国内容易访问的 Gitlab 镜像仓库

OpenCV / opencv · GitCode

        我们不使用 source package,找到可供下载的安装包,直接 binary 安装。

        由于OpenCV安装没有坑,只有版本非常严格,全部使用默认选项完成安装过程即可

4.安装 Visual Studio 2017 并配置

        Visual Studio 是笔者使用的IDE,安装时需要安装所有与Python和C++开发的项目框架。版本差别并不明显,读者可以按自己的习惯使用2017以上的其他版本。

        之后,请参照以下视频,注意工程上选择 x64-debug版本进行安装

Yolo在C++下部署运行+Libtorch+Opencv_哔哩哔哩_bilibili

        其中主要的配置是对VS2017IDE的工程路径设置,笔者自行设置的完成结果可以参考如下:

        属性管理器,Debug | x64,右键属性,VC++目录,包含目录

        C:\Users\xiaoze\Desktop\work1_yolov8Build\用于C++的libtorch\2 libtorch\include\torch\csrc\api\include

        C:\Users\xiaoze\Desktop\work1_yolov8Build\用于C++的libtorch\2 libtorch\include

        C:\Users\xiaoze\Desktop\work1_yolov8Build\用于C++的libtorch\3 opencv\opencv\build\include

        C:\Users\xiaoze\Desktop\work1_yolov8Build\用于C++的libtorch\3 opencv\opencv\build\include\opencv2

属性管理器,Debug | x64,右键属性,VC++目录,包含库目录

        C:\Users\xiaoze\Desktop\work1_yolov8Build\用于C++的libtorch\3 opencv\opencv\build\x64\vc16\lib

        C:\Users\xiaoze\Desktop\work1_yolov8Build\用于C++的libtorch\2 libtorch\lib

属性管理器,Debug | x64,右键属性,链接器,输入,附加依赖项(包含-lib)

其中第1项来自于OpenCV,自第2项至最后一项均来自于Libtorch

opencv_world481d.lib

asmjit.lib

c10.lib

clog.lib

cpuinfo.lib

dnnl.lib

fbgemm.lib

fbjni.lib

fmt.lib

kineto.lib

libprotobuf-lite.lib

libprotobuf.lib

libprotoc.lib

pthreadpool.lib

pytorch_jni.lib

torch.lib

torch_cpu.lib

XNNPACK.lib

请自查依赖的路径和lib库路径数量是否与上述一致,如果少添加了其中的某个库路径,C++程序在编译过程会产生错误。

5.运行最小程序,进行yolo推理测试

文章至此,libtorch和opencv安装,以及VS2017 IDE的环境配置均已完成。如有问题,参考下一部分:《6.过程问题解决》

1、回到Yolov8,首先打开conda prompt,查看目前的虚拟空间

        conda info --envs

        选择对应的conda版本

        conda activate yolov8test

2、通过模型由.pt转换成.onnx格式

        参考:yolov8 opencv模型部署(C++版)_yolov8 c++-CSDN博客

        一般成功以后会看到:

        ONNX: export success 20.1s, saved as 'yolov8stest.onnx' (42.7 MB)

        之后转换成功的 .onnx 文件会出现在根目录里

3、Last Step,调试代码,请参考如下链接:

yolov8 opencv模型部署(C++版)_yolov8 c++-CSDN博客

        请读者自行参考文末的《附录C++ 推理代码》,在VS2017环境中建立工程项目进行测试

三、过程中意料之外的问题解决

1、缺少运行库CRuntime.dll

提示:

"由于找不到vcruntime140_1D.dll,无法继续执行代码。重新安装程序可能会解决此问题。"

这是Microsoft® C Runtime Library 中的一个运行时环境库,不是什么致命问题。此时需要对缺失的dll文件进行下载,请跳转至此页面进行下载:

免费下载缺失的 DLL 文件 | DLL‑files.com

2、opencv_highgui_gtk481_64.dll 报错

中途会出现报错:

libraryLoad load C:\Windows\SYSTEM32\opencv_highgui_gtk481_64.dll => FAILED

处理的方法是禁用WTK

经研究后发现,需要在opencv安装目录下,opencv\source\CMakeList.txt关闭,找到以下词条,将后面的ON改为OFF

OCV_OPTION(WITH_GTK "Include GTK support" OFF

若配置仍无效,则多半为Log打印问题,可以忽视

3、VS2017编译报错:You need C++17 to compile PyTorch

打开Visual Studio,然后打开要设置的项目。

在“解决方案资源管理器”中,右键单击项目,然后选择“属性”。

在项目属性窗口中,展开“配置属性” > “C/C++”,然后选择“语言”。

在右侧的“C++语言标准”下拉菜单中,选择“ISO C++17标准 (/std:c++17)”。

单击“应用”按钮,然后单击“确定”按钮保存更改。

4、VS2017编译报错:c2872 std 不明确的符号

将 属性→ C/C++ → 语言 → 符合模式 改为否,问题解决。

5、VS2017执行报错:由于找不到c10.dll(以及torch_cpu.dll),无法继续执行代码

这是因为执行程序无法找到libtorch/lib下的这些外部动态库,这些库并不是没有,而是程序无法找到他。

最简单粗暴的解决方法就是将你之前下载的libtorch库的libtorch/lib文件夹中的对应.dll文件复制到你目前工程的Example\build\Release

另外,一个有效且简易的方法,就是在属性界面的 调试=>环境 里添加libtorch动态库的路径:

PATH=D:\Code_Lib\libtorch\lib;%PATH%

应用后保存,问题解决。

6、示例程序中,Inference inf("../yolov8n.onnx", cv::Size(640, 480), "classes.txt", runOnGPU); 报错

此处报错有几种可能会导致模型加载错误:

1、模型path错误 --> 检查修改path的绝对路径

2、选择图片path错误 --> 检查修改path的绝对路径

3、c10.dll错误。 --> 检查在vs中修改项目的PATH变量

附录:C++ 推理代码

main.cpp

#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>
#include "inference.h"
using namespace std;
using namespace cv;

int main(int argc, char **argv)
{
    bool runOnGPU = false;

    // 1. 设置你的onnx模型
    // Note that in this example the classes are hard-coded and 'classes.txt' is a place holder.
    Inference inf("../yolov8n.onnx", cv::Size(640, 480), "classes.txt", runOnGPU); // classes.txt 可以缺失

    // 2. 设置你的输入图片
    std::vector<std::string> imageNames;
    imageNames.push_back("../1.jpeg");
    //imageNames.push_back("zidane.jpg");

    for (int i = 0; i < imageNames.size(); ++i)
    {
        cv::Mat frame = cv::imread(imageNames[i]);

        // Inference starts here...
        std::vector<Detection> output = inf.runInference(frame);

        int detections = output.size();
        std::cout << "Number of detections:" << detections << std::endl;

        
        // 若矩形框位置不对,下面这行可能需要
        //cv::resize(frame, frame, cv::Size(480, 640));

        for (int i = 0; i < detections; ++i)
        {
            Detection detection = output[i];

            cv::Rect box = detection.box;
            cv::Scalar color = detection.color;

            // Detection box
            cv::rectangle(frame, box, color, 2);

            // Detection box text
            std::string classString = detection.className + ' ' + std::to_string(detection.confidence).substr(0, 4);
            cv::Size textSize = cv::getTextSize(classString, cv::FONT_HERSHEY_DUPLEX, 1, 2, 0);
            cv::Rect textBox(box.x, box.y - 40, textSize.width + 10, textSize.height + 20);

            cv::rectangle(frame, textBox, color, cv::FILLED);
            cv::putText(frame, classString, cv::Point(box.x + 5, box.y - 10), cv::FONT_HERSHEY_DUPLEX, 1, cv::Scalar(0, 0, 0), 2, 0);
        }
        cv::imwrite("Inference.jpg", frame);
    }
}

inference.h

#ifndef INFERENCE_H
#define INFERENCE_H

// Cpp native
#include <fstream>
#include <vector>
#include <string>
#include <random>

// OpenCV / DNN / Inference
#include <opencv2/imgproc.hpp>
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>

struct Detection
{
    int class_id{0};
    std::string className{};
    float confidence{0.0};
    cv::Scalar color{};
    cv::Rect box{};
};

class Inference
{
public:
    Inference(const std::string &onnxModelPath, const cv::Size &modelInputShape = {640, 640}, const std::string &classesTxtFile = "", const bool &runWithCuda = true);
    std::vector<Detection> runInference(const cv::Mat &input);

private:
    void loadClassesFromFile();
    void loadOnnxNetwork();
    cv::Mat formatToSquare(const cv::Mat &source);

    std::string modelPath{};
    std::string classesPath{};
    bool cudaEnabled{};

    std::vector<std::string> classes{"person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch", "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush"};

    cv::Size2f modelShape{};

    float modelConfidenceThreshold {0.25};
    float modelScoreThreshold      {0.45};
    float modelNMSThreshold        {0.50};

    bool letterBoxForSquare = true;

    cv::dnn::Net net;
};

#endif // INFERENCE_H

inference.cpp

#include "inference.h"

Inference::Inference(const std::string &onnxModelPath, const cv::Size &modelInputShape, const std::string &classesTxtFile, const bool &runWithCuda)
{
    modelPath = onnxModelPath;
    modelShape = modelInputShape;
    classesPath = classesTxtFile;
    cudaEnabled = runWithCuda;

    loadOnnxNetwork();
    // loadClassesFromFile(); The classes are hard-coded for this example
}

std::vector<Detection> Inference::runInference(const cv::Mat &input)
{
    cv::Mat modelInput = input;
    if (letterBoxForSquare && modelShape.width == modelShape.height)
        modelInput = formatToSquare(modelInput);

    cv::Mat blob;
    cv::dnn::blobFromImage(modelInput, blob, 1.0/255.0, modelShape, cv::Scalar(), true, false);
    net.setInput(blob);

    std::vector<cv::Mat> outputs;
    net.forward(outputs, net.getUnconnectedOutLayersNames());

    int rows = outputs[0].size[1];
    int dimensions = outputs[0].size[2];

    bool yolov8 = false;
    // yolov5 has an output of shape (batchSize, 25200, 85) (Num classes + box[x,y,w,h] + confidence[c])
    // yolov8 has an output of shape (batchSize, 84,  8400) (Num classes + box[x,y,w,h])
    if (dimensions > rows) // Check if the shape[2] is more than shape[1] (yolov8)
    {
        yolov8 = true;
        rows = outputs[0].size[2];
        dimensions = outputs[0].size[1];

        outputs[0] = outputs[0].reshape(1, dimensions);
        cv::transpose(outputs[0], outputs[0]);
    }
    float *data = (float *)outputs[0].data;

    float x_factor = modelInput.cols / modelShape.width;
    float y_factor = modelInput.rows / modelShape.height;
    //std::cout<<modelInput.cols <<" "<<modelShape.width<<" "<<modelInput.rows<<" "<<modelShape.height<<std::endl;

    std::vector<int> class_ids;
    std::vector<float> confidences;
    std::vector<cv::Rect> boxes;
    std::cout<<yolov8<<std::endl;
    for (int i = 0; i < rows; ++i)
    {
        if (yolov8)
        {
            float *classes_scores = data+4;

            cv::Mat scores(1, classes.size(), CV_32FC1, classes_scores);
            cv::Point class_id;
            double maxClassScore;

            minMaxLoc(scores, 0, &maxClassScore, 0, &class_id);

            if (maxClassScore > modelScoreThreshold)
            {
                confidences.push_back(maxClassScore);
                class_ids.push_back(class_id.x);

                float x = data[0];
                float y = data[1];
                float w = data[2];
                float h = data[3];

                int left = int((x - 0.5 * w) * x_factor);
                int top = int((y - 0.5 * h) * y_factor);

                int width = int(w * x_factor);
                int height = int(h * y_factor);
        //std::cout<<left<<" "<<top<<" "<<width<<" "<<height<<std::endl;

                boxes.push_back(cv::Rect(left, top, width, height));
            }
        }
        else // yolov5
        {
            float confidence = data[4];

            if (confidence >= modelConfidenceThreshold)
            {
                float *classes_scores = data+5;

                cv::Mat scores(1, classes.size(), CV_32FC1, classes_scores);
                cv::Point class_id;
                double max_class_score;

                minMaxLoc(scores, 0, &max_class_score, 0, &class_id);

                if (max_class_score > modelScoreThreshold)
                {
                    confidences.push_back(confidence);
                    class_ids.push_back(class_id.x);

                    float x = data[0];
                    float y = data[1];
                    float w = data[2];
                    float h = data[3];

                    int left = int((x - 0.5 * w) * x_factor);
                    int top = int((y - 0.5 * h) * y_factor);

                    int width = int(w * x_factor);
                    int height = int(h * y_factor);

                    boxes.push_back(cv::Rect(left, top, width, height));
                }
            }
        }

        data += dimensions;
    }

    std::vector<int> nms_result;
    cv::dnn::NMSBoxes(boxes, confidences, modelScoreThreshold, modelNMSThreshold, nms_result);

    std::vector<Detection> detections{};
    for (unsigned long i = 0; i < nms_result.size(); ++i)
    {
        int idx = nms_result[i];

        Detection result;
        result.class_id = class_ids[idx];
        result.confidence = confidences[idx];

        std::random_device rd;
        std::mt19937 gen(rd());
        std::uniform_int_distribution<int> dis(100, 255);
        result.color = cv::Scalar(dis(gen),
                                  dis(gen),
                                  dis(gen));

        result.className = classes[result.class_id];
        result.box = boxes[idx];

        detections.push_back(result);
    }

    return detections;
}

void Inference::loadClassesFromFile()
{
    std::ifstream inputFile(classesPath);
    if (inputFile.is_open())
    {
        std::string classLine;
        while (std::getline(inputFile, classLine))
            classes.push_back(classLine);
        inputFile.close();
    }
}

void Inference::loadOnnxNetwork()
{
    net = cv::dnn::readNetFromONNX(modelPath);
    if (cudaEnabled)
    {
        std::cout << "\nRunning on CUDA" << std::endl;
        net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
        net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);
    }
    else
    {
        std::cout << "\nRunning on CPU" << std::endl;
        net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
        net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);
    }
}

cv::Mat Inference::formatToSquare(const cv::Mat &source)
{
    int col = source.cols;
    int row = source.rows;
    int _max = MAX(col, row);
    cv::Mat result = cv::Mat::zeros(_max, _max, CV_8UC3);
    source.copyTo(result(cv::Rect(0, 0, col, row)));
    return result;
}

  • 31
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值