[YOLOv8] 在Jetson Nano当中部署YOLOv8目标检测算法

源码下载

本文所有源码在GitHub当中发布,喜欢的话麻烦点个star

GitHub - Dominic23331/yolov8_tensorrt

前期准备

硬件准备

在前期的准备当中,你需要有以下硬件:

  1. 个人PC
  2. Jetson Nano开发板
  3. TF卡
  4. 路由器
  5. 网线

软件准备

在个人电脑当中,你需要安装以下软件:

  1. Anaconda
  2. Pytorch
  3. ultralytics
  4. MobaXtern
  5. balenaEtcher
  6. Clion
  7. Pycharm(可选)

接下来主要讲解一下jetson nano当中的软件安装过程。

1、首先,你需要准备一个 Jetson Nano(B01或A02 均可,建议采用 4GB 版的开发板),以及一张容量大于 16GB 的 TF 卡。

jetson nano
TF卡

2、在英伟达网站中下载jetson nano的镜像文件。

下载镜像

3、使用balenaEtcher软件将镜像写入TF卡当中。

烧录镜像

4、将TF卡插入jetson nano当中,并进行初始化设置。

jetson nano界面

5、 使用网线或无线连接将 Jetson Nano 与路由器相连接,然后在浏览器中输入路由器的管理界面地址(通常是 192.168.0.1)。从管理界面中查找 Jetson Nano 的 IP 地址。

查找jetson nano的ip地址

6、使用 MobaXterm 软件的 SSH 功能,输入 Jetson Nano 的 IP 地址以及账号密码,连接到 Jetson Nano。

连接jetson nano
连接效果

7、(可选)为了方便之后的模型转换操作,建议将trtexec加入环境变量中。

sudo gedit ~/.bashrc

# 在.bashrc当中添加以下内容

export PATH=/usr/src/tensorrt/bin:$PATH

# 更新环境变量

source ~/.bashrc

远程开发的准备

为了方便开发,我们将使用 Clion 配置远程开发,这样就可以在个人电脑上编写 Jetson Nano 的代码。在配置过程中,请确保 Jetson Nano 能够稳定连接到路由器,建议使用网线连接以提高稳定性。以下是配置步骤:

1、打开Clion,并创建一个工程,在菜单栏中选择setting,并打开build,Execution,Deployment->Toolchains。在Toolchains当中新建一个Remote Host,通过指引配置jetson nano的远程开发环境。

构建远程开发环境

2、点击上图红圈位置以打开远程服务器的配置栏,然后输入 Jetson Nano 的 IP 地址和账号密码来建立连接,这样就可以在 Clion 中进行对 Jetson Nano 的远程开发了。

配置jetson nano远程开发

 模型的转换

pt转换为onnx

为了将模型部署到jetson nano当中,我们首先需要将需要转换的模型导出为onnx格式。首先,你需要下载YOLOv8的模型文件,模型可以从这里下载。由于jetson nano的GPU计算能力较弱,在这里我使用了YOLOv8n模型,并将输入图像的尺寸缩小为原来的四分之一。转换的代码如下所示:

from ultralytics import YOLO

model = YOLO("yolov8n.pt")
model.export(imgsz=320, format='onnx')

这样,我们就得到了onnx格式的YOLOv8模型了。

onnx转换为engine

在 Jetson Nano 上,我们可以使用 TensorRT 对模型进行加速并部署。由于 TensorRT 对模型的优化与硬件有关,因此需要将 ONNX 模型上传至 Jetson Nano,并通过 trtexec 工具进行模型的转换。以下是模型转换的命令:

trtexec --onnx=onnx模型路径 --saveEngine=模型保存路径

通过以上操作,即可将YOLOv8模型转换为jetson nano支持的格式了。

模型的部署

前期准备

在模型部署当中,首先我们需要将模型文件读取至内存当中,并初始化TensorRT模型,为了方便编程,本文将YOLOv8模型封装为一个类当中,通过构造函数的方式对模型进行初始化操作。

YOLO::YOLO(const std::string &model_path, nvinfer1::ILogger &logger) {
    // load engine file
    std::ifstream engineStream(model_path, std::ios::binary);

    // check engine file exist.
    if (!engineStream.is_open()) {
        std::cout << "Cannot find model from: " + model_path << std::endl;
        exit(0);
    }

    engineStream.seekg(0, std::ios::end);
    const size_t modelSize = engineStream.tellg();
    engineStream.seekg(0, std::ios::beg);
    std::unique_ptr<char[]> engineData(new char[modelSize]);
    engineStream.read(engineData.get(), modelSize);
    engineStream.close();

    runtime = nvinfer1::createInferRuntime(logger);
    engine = runtime->deserializeCudaEngine(engineData.get(), modelSize);
    context = engine->createExecutionContext();

    context->setBindingDimensions(0, nvinfer1::Dims4(1, 3, input_h, input_w));

    cudaStreamCreate(&stream);

    offset[0] = 0;
    offset[1] = 0;

    // Calculate output dimension by inputting image size
    out_dim_2 = (input_w / 8) * (input_h / 8) + (input_w / 16) * (input_h / 16) + (input_w / 32) * (input_h / 32);
    boxes_result.resize(out_dim_2 * 84);
}

为了展示加载的模型的输入输出,我们编写了一个函数,用于打印模型的输入和输出信息。

void YOLO::show() {
    for (int i = 0; i < engine->getNbBindings(); i++) {
        std::cout << "node: " << engine->getBindingName(i) << ", ";
        if (engine->bindingIsInput(i)) {
            std::cout << "type: input" << ", ";
        } else {
            std::cout << "type: output" << ", ";
        }
        nvinfer1::Dims dim = engine->getBindingDimensions(i);
        std::cout << "dimensions: ";
        for (int d = 0; d < dim.nbDims; d++) {
            std::cout << dim.d[d] << " ";
        }
        std::cout << "\n";
    }
}

/**输出结果
node: images, type: input, dimensions: 1 3 320 320 
node: output0, type: output, dimensions: 1 84 2100 
*/

接下来,我们编写一个预热函数,通过喂入模型随机数据,使模型在GPU当中预热,加快模型的推理速度。

void YOLO::warmup(int epoch)
{
    std::cout << "Warm up." << std::endl;

    for (int step = 0; step <= epoch; ++step)
    {
        // use a random tensor to warmup
        cv::Mat randomImage(input_h, input_w, CV_8UC3);
        cv::randu(randomImage, cv::Scalar::all(0), cv::Scalar::all(255));
        run(randomImage);

        printProgressBar(step, epoch);
    }
    std::cout << std::endl;
}

通过以上代码,经过多轮预热过后,模型能够顺畅的在GPU当中计算。

模型推理

接下来,我们编写模型的推理代码,推理代码主要由预处理,模型推理以及后处理所构成,在预处理阶段,代码首先会将输入的图像进行resize,将图像调整为320x320大小。由于OpenCV在读取图像时,会以BGR的顺序将图像读入内存,因此在输入到模型前,应将图像从BGR格式转换为RGB的格式,并将每一个像素除以255。以下是预处理代码:

std::vector<float> YOLO::preprocess(cv::Mat &image)
{
    // get resized image
    std::tuple<cv::Mat, int, int> resized = resize(image, input_w, input_h);
    cv::Mat resized_image = std::get<0>(resized);

    // get resize offset
    offset[0] = std::get<1>(resized);
    offset[1] = std::get<2>(resized);

    // BGR2RGB
    cv::cvtColor(resized_image, resized_image, cv::COLOR_BGR2RGB);

    std::vector<float> input_tensor;
    for (int k = 0; k < 3; k++) {
        for (int i = 0; i < resized_image.rows; i++) {
            for (int j = 0; j < resized_image.cols; j++) {
                input_tensor.emplace_back(((float) resized_image.at<cv::Vec3b>(i, j)[k]) / 255.);
            }
        }
    }

    return input_tensor;
}

在模型的推理阶段,我们所要做的是将预处理的图像从CPU当中转到GPU中,并将图像输入到模型当中,经过模型的推理之后,再将推理的结果从GPU搬运到CPU当中进行后处理的计算。以下代码为推理程序:

// upload to cuda
    cudaMalloc(&buffer[0], 3 * input_h * input_w * sizeof(float));
    cudaMalloc(&buffer[1], out_dim_2 * 84 * sizeof(float));
    cudaMemcpyAsync(buffer[0], input.data(), 3 * input_h * input_w * sizeof(float), cudaMemcpyHostToDevice, stream);

    // inference
    context->enqueueV2(buffer, stream, nullptr);
    cudaStreamSynchronize(stream);

    // download from cuda
    cudaMemcpyAsync(boxes_result.data(), buffer[1], out_dim_2 * 84 * sizeof(float), cudaMemcpyDeviceToHost);

经过了模型的推理,我们可以获得模型输出的所有的检测框,由于模型输出的维度为(1,84,2100),而TensorRT输出的结果为一个一维向量,因此需要通过两个for循环,对模型的输出进行解码。

std::vector<float> b;
    for (int i=0; i < out_dim_2; i++)
    {
        b.clear();
        for (int j=0; j < 84; j++)
        {
            b.push_back(tensor[j * out_dim_2 + i]);
        }

        b = decode_cls(b);
        if (b[4] < conf_threshold)
            continue;

        Box db;
        db.x1 = b[0] - b[2] / 2;
        db.y1 = b[1] - b[3] / 2;
        db.x2 = b[0] + b[2] / 2;
        db.y2 = b[1] + b[3] / 2;
        db.conf = b[4];
        db.cls = classes[(int) b[5]];

        boxes.push_back(db);
    }

我们通过以上代码,即可得到大于置信度阈值的所有检测框。由于检测框当中存在重叠,因此需要通过非极大值抑制算法,对冗余的检测框进行过滤。以下是算法的代码:

/**
 * decode classes
 * @param box output box
 * @return boxes
 */
std::vector<float> decode_cls(std::vector<float>& box)
{
    std::vector<float> cls_list(box.begin() + 4, box.end());
    float conf = *std::max_element(cls_list.begin(), cls_list.end());
    float cls = std::max_element(cls_list.begin(), cls_list.end()) - cls_list.begin();

    std::vector<float> result(box.begin(), box.begin() + 4);
    result.push_back(conf);
    result.push_back(cls);

    return result;
}


/**
 * compare two boxes
 * @param b1 box1
 * @param b2 box2
 * @return result
 */
bool compare_boxes(const Box& b1, const Box& b2)
{
    return b1.conf < b2.conf;
}


/**
 * compare two boxes IOU
 * @param b1 box1
 * @param b2 box2
 * @return iou
 */
float intersection_over_union(const Box& b1, const Box& b2)
{
    float x1 = std::max(b1.x1, b2.x1);
    float y1 = std::max(b1.y1, b2.y1);
    float x2 = std::min(b1.x2, b2.x2);
    float y2 = std::min(b1.y2, b2.y2);

    // get intersection
    float box_intersection = std::max((float)0, x2 - x1) * std::max((float)0, y2 - y1);

    // get union
    float area1 = (b1.x2 - b1.x1) * (b1.y2 - b1.y1);
    float area2 = (b2.x2 - b2.x1) * (b2.y2 - b2.y1);
    float box_union = area1 + area2 - box_intersection;

    // To prevent the denominator from being zero, add a very small numerical value to the denominator
    float iou = box_intersection / (box_union + 0.0001);

    return iou;
}


/**
 * NMS
 * @param boxes output boxes from YOLO
 * @param iou_thre  iou threshold
 * @return boxes after NMS
 */
std::vector<Box> non_maximum_suppression(std::vector<Box> boxes, float iou_thre)
{
    // Sort boxes based on confidence
    std::sort(boxes.begin(), boxes.end(), compare_boxes);

    std::vector<Box> result;
    std::vector<Box> temp;
    while (!boxes.empty())
    {
        temp.clear();

        Box chosen_box = boxes.back();
        boxes.pop_back();
        for (auto & boxe : boxes)
        {
            if (boxe.cls != chosen_box.cls || intersection_over_union(boxe, chosen_box) < iou_thre)
                temp.push_back(boxe);
        }

        boxes = temp;
        result.push_back(chosen_box);
    }
    return result;
}

最后,即可将输出的检测框还原至原图的尺寸。

// resize boxes
for (auto & boxe : boxes)
{
    boxe.x1 = MAX((boxe.x1 - offset[0]) * img_w / (input_w - 2 * offset[0]), 0);
    boxe.y1 = MAX((boxe.y1 - offset[1]) * img_h / (input_h - 2 * offset[1]), 0);
    boxe.x2 = MIN((boxe.x2 - offset[0]) * img_w / (input_w - 2 * offset[0]), img_w);
    boxe.y2 = MIN((boxe.y2 - offset[1]) * img_h / (input_h - 2 * offset[1]), img_h);
}

 我们将图像输入模型,即可得到结果:

输入的图像

总结

经过上述步骤,我们就完成了在jetson nano当中部署YOLOv8的工作。为了检验其效率,我编写了代码对每个步骤的耗时进行统计,以下是统计结果:

Waste time: 0.07
Preprocess time: 0.03
Cuda upload time: 0.00
Model inference time: 0.01
Cuda download time: 0.00
Postprocess time: 0.03
FPS: 14.38

 由结果可见,推理一张图像的总耗时大概70ms左右,cuda搬运数据几乎不耗时,大概是因为jetson nano的GPU和CPU共用一个内存吧。而令人困惑的是,模型的推理时间只有10ms,反而前后处理消耗了60ms的时间,可能是包含了多个大循环所导致的,接下来将尝试使用CUDA对前后处理进行加速。

  • 16
    点赞
  • 58
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
YOLOv5是一种目标检测深度学习算法,它在推理速度和精度上都有较好的性能。要在Jetson Nano上部署YOLOv5目标检测,可以按照以下步骤进行操作: 1. 首先,需要将训练好的YOLOv5s权重文件转换为wts文件格式。然后,使用build工具将wts文件转换为TensorRT的engine文件。这样就可以对输入的Tensors进行加速处理并得到推理的输出结果。\[2\] 2. 在Jetson Nano上部署YOLOv5目标检测,还需要修改文件夹DeepStream-Yolo/external/yolov5/nvdsinfer_custom_impl_Yolo下的文件yololayer.h和yololayer.cu,以使其可以在YOLOv5 V4.0上正常工作。\[3\] 通过以上步骤,你就可以在Jetson Nano上成功部署YOLOv5目标检测算法了。希望对你有所帮助! #### 引用[.reference_title] - *1* *2* [Jetson Nano 部署(1):YOLOv5 目标检测实战介绍](https://blog.csdn.net/weixin_38346042/article/details/126399006)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Jetson Nano部署实现(一)——Yolov5目标检测-Jetson Nano部署](https://blog.csdn.net/qq_40305597/article/details/117320573)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值