基于正点原子阿尔法开发板(IMX6ULL)做yolov11n轻量化部署

0. 补充内容

0.1 ncnn简介

ncnn 是腾讯优图实验室开源的、专为移动端 / 嵌入式设备极致优化的高性能神经网络前向推理框架,核心定位是把训练好的深度学习模型高效部署到手机、平板、边缘设备上做实时预测,不负责训练,只负责推理。

0.2 param 和 bin 文件是什么

param 和 bin 文件是 ncnn 模型文件的两部分组成。这两部分文件一起构成了完整的 ncnn 模型,可以被 ncnn 框架加载和使用进行推理任务。

param 文件包含了模型的结构信息和参数配置,而 bin 文件则包含了模型的权重数据。

0.3 opencv简介

OpenCV(Open Source Computer Vision Library)是一个开源的计算机视觉和机器学习软件库,提供了丰富的函数和工具,用于图像处理、视频分析、特征检测、对象识别等任务。OpenCV 支持多种编程语言,包括 C++、Python 和 Java,并且可以在多个平台上运行,如 Windows、Linux 和 macOS。它广泛应用于各种领域,如机器人、自动驾驶、医疗影像分析等。

1. 具体实操

1.1 硬件准备

  • 正点原子阿尔法开发板(IMX6ULL)

  • SD卡(至少16GB)

1.2 环境搭建

我们可以先在虚拟机里面创建一个文件夹来做环境隔离,这个文件夹就是我们后续编译和开发的工作空间,所有的文件都放在这个文件夹里,方便管理。

在这里插入图片描述

我创建了一个文件夹为yolo,然后我把ncnn和opencv的源码都放在这个文件夹里,后续我们在这个文件夹里进行编译和开发。

1.2.1 ncnn 框架

PC 端交叉编译 NCNN

①拉去ncnn源码


sudo apt install git

git clone https://github.com/Tencent/ncnn.git

//或者使用国内镜像

git clone https://github.com.cnpmjs.org/Tencent/ncnn.git

//或者在自己电脑pc下载拖过去

https://codeload.github.com/Tencent/ncnn/zip/refs/heads/master

unzip ncnn-master.zip

cd ncnn-master

②按装依赖


sudo apt update

sudo apt install build-essential git cmake libprotobuf-dev protobuf-compiler libopencv-dev

sudo apt install cmake gcc g++ libopencv-dev protobuf-compiler libprotobuf-dev

③创建编译目录并配置cmake

创建一个文件夹build,然后进入这个文件夹开始编译


mkdir build && cd build

注意这里的编译器是刚刚新下载的x86的编译器,编译ncnn的,不是平时那个交叉编译器


cmake .. -DCMAKE_TOOLCHAIN_FILE=../toolchains/arm-linux-gnueabihf.toolchain.cmake -DCMAKE_BUILD_TYPE=Release -DNCNN_VULKAN=OFF

交叉编译时线程库(pthread/OpenMP)检测失败,这是 IMX6ULL 交叉编译的常见问题。


cmake .. -DCMAKE_TOOLCHAIN_FILE=../toolchains/arm-linux-gnueabihf.toolchain.cmake -DCMAKE_BUILD_TYPE=Release -DNCNN_VULKAN=OFF -DNCNN_OPENMP=OFF -DCMAKE_DISABLE_FIND_PACKAGE_PTHREAD=TRUE

然后我们就可以进入build文件夹,开始编译了


make -j4

这一步要编译很久,可能会有警告,但是没出现红色的报错都可以继续运行

编译成功后,build 文件夹里会产出 2 类核心文件(你只需要关心这两个):

Ⅰ、【最重要】ncnn 库文件

路径:ncnn-master/build/src/libncnn.a(相对路径,可以根据自己的文件夹位置查看)

这是 静态库

你在 IMX6ULL 上跑 AI 模型 必须用这个文件

Ⅱ、头文件目录

整个文件夹:ncnn-master/src/

里面有:plaintext、ncnn.h、modelbin.h

编译自己的程序时,必须包含这些头文件

④ 测试开发板

编译完成后,在build-imx6ull目录下,我们可以看到benchmark目录和example目录,前者目录中的benchncnn就是 NCNN 的基准测试工具,后者目录中是编译完成的样例程序:

我们可以先把 benchmark 目录下的 benchncnn 这个工具复制到开发板上,测试一下开发板的性能,看看能不能正常运行。

在这里插入图片描述

这里运行可能会等待一会,运行命令:


//进入这个可执行文件所在sd卡目录

./benchncnn

在这里插入图片描述

我们可以看到运行结果,说明开发板的环境搭建成功了,可以正常运行 ncnn 的程序了。最下面的报错是因为这个工具需要 Vulkan 支持,而我们的开发板不支持 Vulkan,所以这个报错是正常的,不影响我们后续的开发。

1.2.2 准备.parapm和.bin文件

我们需要把训练好的模型转换成 ncnn 支持的格式,也就是 .param 和 .bin 文件。这个过程需要使用 ncnn 提供的工具来完成。

我们训练自己的模型,去官网上下载yolo的文件,有一个requirements.txt文件,里面有一些依赖库,我们需要安装这些依赖库,安装完成后我们就可以训练自己的模型了,训练完成后我们会得到一个.pt格式的模型文件,我们需要把这个.pt文件转换成.onnx格式的文件,转换完成后我们就可以使用 ncnn 提供的工具来把 .onnx 文件转换成 .param 和 .bin 文件了。(详细原理和步骤很多,可以自寻查找网上很多开源案例详细步骤)我这里就直接下载yolo的预训练模型,已经转换好的 .param 和 .bin 文件,直接拿来用就可以了。

下载链接:


Invoke-WebRequest -Uri "https://labfile.oss-cn-hangzhou.aliyuncs.com/models/yolov11n.param" -OutFile "yolov11n.param"

Invoke-WebRequest -Uri "https://labfile.oss-cn-hangzhou.aliyuncs.com/models/yolov11n.bin" -OutFile "yolov11n.bin"

当然也可以下载其他的模型,或者自己训练的模型,只要把它转换成 ncnn 支持的 .param 和 .bin 文件就可以了。然后我这里由于.cpp文件是写的yolov11n的,所以我就直接下载了yolov11n的模型文件,直接拿来用就可以了。

1.2.3 安装opencv

opencv 的安装和使用也很简单,我们可以直接使用 apt 来安装 opencv 的开发包,这样我们就可以在编译自己的程序时链接 opencv 的库了。

编译arm版本的opencv,这里一定是arm版本的,因为我们的阿尔法IMX6ULL开发板是arm架构的,如果安装了x86版本的opencv,是无法在开发板上运行的,所以一定要安装arm版本的opencv


// 编译arm版本的opencv

wget https://github.com/opencv/opencv/archive/3.4.9.zip

unzip 3.4.9.zip && cd opencv-3.4.9

// 创建构建目录

mkdir build-arm && cd build-arm

// 配置交叉编译

cmake \

  -DCMAKE_TOOLCHAIN_FILE=../platforms/linux/arm-gnueabi.toolchain.cmake \

  -DCMAKE_INSTALL_PREFIX=/opt/opencv-arm \

  -DBUILD_LIST=core,highgui,imgcodecs,imgproc \

  -DWITH_GTK=OFF \

  -DWITH_JPEG=ON \

  -DWITH_PNG=ON ..

// 编译并安装

make -j$(nproc) && sudo make install

安装好后,我们就可以在编译自己的程序时链接 opencv 的库了,链接的时候需要指定 opencv 的头文件和库文件的路径,这样编译器才能找到 opencv 的相关文件。

同时我们要在/opt/opencv-arm/lib/目录下找到 opencv 的库文件,这些库文件就是我们在编译自己的程序时需要链接的库文件。我们需要把这个库文件放到我们的开发板上,这样我们在开发板上运行自己的程序时才能找到 opencv 的库文件。我们可以把这个库文件放到开发板的 /usr/lib/ 目录下,这样我们在开发板上运行自己的程序时就可以找到 opencv 的库文件了。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

我们把路径为/opt/opencv-arm/lib/目录下的.so.3.4.9这个文件,通过sd卡复制到开发板的/usr/lib/目录下,这样我们在开发板上运行自己的程序时就可以找到 opencv 的库文件了。

先把这几个文件拖入sd卡,插入sd卡到开发板上,然后把这几个文件复制到/usr/lib/目录下,命令如下:


cp /media/sdcard/libopencv_core.so.3.4.9 /usr/lib/

cp /media/sdcard/libopencv_highgui.so.3.4.9 /usr/lib/

cp /media/sdcard/libopencv_imgcodecs.so.3.4.9 /usr/lib/

cp /media/sdcard/libopencv_imgproc.so.3.4.9 /usr/lib/

然后使用软连接的方式把这些库文件链接到/usr/lib/目录下,这样我们在开发板上运行自己的程序时就可以找到 opencv 的库文件了,命令如下:


ln -s /usr/lib/libopencv_core.so.3.4.9 /usr/lib/libopencv_core.so

ln -s /usr/lib/libopencv_highgui.so.3.4.9 /usr/lib/libopencv_highgui.so

ln -s /usr/lib/libopencv_imgcodecs.so.3.4.9 /usr/lib/libopencv_imgcodecs.so

ln -s /usr/lib/libopencv_imgproc.so.3.4.9 /usr/lib/libopencv_imgproc.so

1.2.4 编译、运行自己的程序

我们写一个.cpp文件来测试一下 ncnn 和 opencv 的功能,这个文件里我们会使用 ncnn 来加载 .param 和 .bin 文件来进行推理。

我们在编译自己的程序时,需要指定 ncnn 的头文件和库文件的路径,这样编译器才能找到 ncnn 的相关文件。我们还需要指定 opencv 的头文件和库文件的路径,这样编译器才能找到 opencv 的相关文件。我们还需要指定交叉编译器的路径,这样编译器才能使用交叉编译器来编译我们的程序。

①我们在yolo文件夹下(装了刚刚那些环境的文件夹)创建一个.cpp文件

yolov11n.cpp文件的内容如下:


// Tencent is pleased to support the open source community by making ncnn

// available.

//

// Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved.

//

// Copyright (C) 2024 whyb(https://github.com/whyb). All rights reserved.

//

// Copyright (C) 2024 HexRx. All rights reserved.

//

// Licensed under the BSD 3-Clause License (the "License"); you may not use this

// file except in compliance with the License. You may obtain a copy of the

// License at

//

// https://opensource.org/licenses/BSD-3-Clause

//

// Unless required by applicable law or agreed to in writing, software

// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT

// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the

// License for the specific language governing permissions and limitations under

// the License.

#include <float.h>

#include <stdio.h>

#include <algorithm>

#include <memory>

#include <opencv2/core/core.hpp>

#include <opencv2/highgui/highgui.hpp>

#include <opencv2/opencv.hpp>

#include <vector>

#include <iostream>

#include "layer.h"

#include "net.h"

#define MAX_STRIDE 32

static const char *class_names[] = {"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"};

struct Object

{

    cv::Rect_<float> rect;

    int label;

    float prob;

};

static inline float intersection_area(const Object &a, const Object &b)

{

    cv::Rect_<float> inter = a.rect & b.rect;

    return inter.area();

}

static void qsort_descent_inplace(std::vector<Object> &objects, int left, int right)

{

    int i = left;

    int j = right;

    float p = objects[(left + right) / 2].prob;

    while (i <= j)

    {

        while (objects[i].prob > p)

            i++;

        while (objects[j].prob < p)

            j--;

        if (i <= j)

        {

            // swap

            std::swap(objects[i], objects[j]);

            i++;

            j--;

        }

    }

#pragma omp parallel sections

    {

#pragma omp section

        {

            if (left < j)

                qsort_descent_inplace(objects, left, j);

        }

#pragma omp section

        {

            if (i < right)

                qsort_descent_inplace(objects, i, right);

        }

    }

}

static void qsort_descent_inplace(std::vector<Object> &objects)

{

    if (objects.empty())

        return;

    qsort_descent_inplace(objects, 0, objects.size() - 1);

}

static void nms_sorted_bboxes(const std::vector<Object> &faceobjects, std::vector<int> &picked,

                              float nms_threshold, bool agnostic = false)

{

    picked.clear();

    const int n = faceobjects.size();

    std::vector<float> areas(n);

    for (int i = 0; i < n; i++)

    {

        areas[i] = faceobjects[i].rect.area();

    }

    for (int i = 0; i < n; i++)

    {

        const Object &a = faceobjects[i];

        int keep = 1;

        for (int j = 0; j < (int)picked.size(); j++)

        {

            const Object &b = faceobjects[picked[j]];

            if (!agnostic && a.label != b.label)

                continue;

            // intersection over union

            float inter_area = intersection_area(a, b);

            float union_area = areas[i] + areas[picked[j]] - inter_area;

            // float IoU = inter_area / union_area

            if (inter_area / union_area > nms_threshold)

                keep = 0;

        }

        if (keep)

            picked.push_back(i);

    }

}

static inline float sigmoid(float x) { return static_cast<float>(1.f / (1.f + exp(-x))); }

static inline float clampf(float d, float min, float max)

{

    const float t = d < min ? min : d;

    return t > max ? max : t;

}

static void parse_yolo11_detections(float *inputs, float confidence_threshold, int num_channels,

                                    int num_anchors, int num_labels, int infer_img_width,

                                    int infer_img_height, std::vector<Object> &objects)

{

    std::vector<Object> detections;

    cv::Mat output = cv::Mat((int)num_channels, (int)num_anchors, CV_32F, inputs).t();

    std::cout << "output shape: [" << output.rows << ", " << output.cols << "]" << std::endl;

    for (int i = 0; i < num_anchors; i++)

    {

        const float *row_ptr = output.row(i).ptr<float>();

        const float *bboxes_ptr = row_ptr;

        const float *scores_ptr = row_ptr + 4;

        const float *max_s_ptr = std::max_element(scores_ptr, scores_ptr + num_labels);

        float score = *max_s_ptr;

        if (score > confidence_threshold)

        {

            float x = *bboxes_ptr++;

            float y = *bboxes_ptr++;

            float w = *bboxes_ptr++;

            float h = *bboxes_ptr;

            float x0 = clampf((x - 0.5f * w), 0.f, (float)infer_img_width);

            float y0 = clampf((y - 0.5f * h), 0.f, (float)infer_img_height);

            float x1 = clampf((x + 0.5f * w), 0.f, (float)infer_img_width);

            float y1 = clampf((y + 0.5f * h), 0.f, (float)infer_img_height);

            cv::Rect_<float> bbox;

            bbox.x = x0;

            bbox.y = y0;

            bbox.width = x1 - x0;

            bbox.height = y1 - y0;

            Object object;

            object.label = max_s_ptr - scores_ptr;

            object.prob = score;

            object.rect = bbox;

            detections.push_back(object);

        }

    }

    objects = detections;

}

static int detect_yolo11(const char *param_path, const char *modelpath, const cv::Mat &bgr,

                         std::vector<Object> &objects)

{

    ncnn::Net yolo11;

    yolo11.opt.use_vulkan_compute = true; // if you want detect in hardware, then enable it

    yolo11.load_param(param_path);

    yolo11.load_model(modelpath);

    const int target_size = 640;

    const float prob_threshold = 0.25f;

    const float nms_threshold = 0.45f;

    int img_w = bgr.cols;

    int img_h = bgr.rows;

    // letterbox pad to multiple of MAX_STRIDE

    int w = img_w;

    int h = img_h;

    float scale = 1.f;

    if (w > h)

    {

        scale = (float)target_size / w;

        w = target_size;

        h = h * scale;

    }

    else

    {

        scale = (float)target_size / h;

        h = target_size;

        w = w * scale;

    }

    ncnn::Mat in =

        ncnn::Mat::from_pixels_resize(bgr.data, ncnn::Mat::PIXEL_BGR2RGB, img_w, img_h, w, h);

    int wpad = (target_size + MAX_STRIDE - 1) / MAX_STRIDE * MAX_STRIDE - w;

    int hpad = (target_size + MAX_STRIDE - 1) / MAX_STRIDE * MAX_STRIDE - h;

    ncnn::Mat in_pad;

    ncnn::copy_make_border(in, in_pad, hpad / 2, hpad - hpad / 2, wpad / 2, wpad - wpad / 2,

                           ncnn::BORDER_CONSTANT, 114.f);

    const float norm_vals[3] = {1 / 255.f, 1 / 255.f, 1 / 255.f};

    in_pad.substract_mean_normalize(0, norm_vals);

    ncnn::Extractor ex = yolo11.create_extractor();

    std::cout << "in0 Shape: ["

              << in_pad.w << ", " // 宽度(第1维度)

              << in_pad.h << ", " // 高度(第2维度)

              << in_pad.d << ", " // 深度(第3维度)

              << in_pad.c << "]"  // 通道数(第4维度)

              << std::endl;

    ex.input("in0", in_pad);

    std::vector<Object> proposals;

    // stride 32

    {

        ncnn::Mat out;

        ex.extract("out0", out);

        std::cout << "pred Shape: ["

                  << out.w << ", " // 宽度(第1维度)8400

                  << out.h << ", " // 高度(第2维度)84

                  << out.d << ", " // 深度(第3维度)1

                  << out.c << "]"  // 通道数(第4维度)1

                  << std::endl;

        std::vector<Object> objects32;

        const int num_labels = sizeof(class_names) / sizeof(class_names[0]);

        parse_yolo11_detections((float *)out.data, prob_threshold, out.h, out.w, num_labels, in_pad.w,

                                in_pad.h, objects32);

        proposals.insert(proposals.end(), objects32.begin(), objects32.end());

    }

    // sort all proposals by score from highest to lowest

    qsort_descent_inplace(proposals);

    // apply nms with nms_threshold

    std::vector<int> picked;

    nms_sorted_bboxes(proposals, picked, nms_threshold);

    int count = picked.size();

    objects.resize(count);

    for (int i = 0; i < count; i++)

    {

        objects[i] = proposals[picked[i]];

        // adjust offset to original unpadded

        float x0 = (objects[i].rect.x - (wpad / 2)) / scale;

        float y0 = (objects[i].rect.y - (hpad / 2)) / scale;

        float x1 = (objects[i].rect.x + objects[i].rect.width - (wpad / 2)) / scale;

        float y1 = (objects[i].rect.y + objects[i].rect.height - (hpad / 2)) / scale;

        // clip

        x0 = std::max(std::min(x0, (float)(img_w - 1)), 0.f);

        y0 = std::max(std::min(y0, (float)(img_h - 1)), 0.f);

        x1 = std::max(std::min(x1, (float)(img_w - 1)), 0.f);

        y1 = std::max(std::min(y1, (float)(img_h - 1)), 0.f);

        objects[i].rect.x = x0;

        objects[i].rect.y = y0;

        objects[i].rect.width = x1 - x0;

        objects[i].rect.height = y1 - y0;

    }

    return 0;

}

static void draw_objects(const cv::Mat &bgr, const std::vector<Object> &objects)

{

    static const unsigned char colors[19][3] = {

        {54, 67, 244}, {99, 30, 233}, {176, 39, 156}, {183, 58, 103}, {181, 81, 63}, {243, 150, 33}, {244, 169, 3}, {212, 188, 0}, {136, 150, 0}, {80, 175, 76}, {74, 195, 139}, {57, 220, 205}, {59, 235, 255}, {7, 193, 255}, {0, 152, 255}, {34, 87, 255}, {72, 85, 121}, {158, 158, 158}, {139, 125, 96}};

    int color_index = 0;

    cv::Mat image = bgr.clone();

    for (size_t i = 0; i < objects.size(); i++)

    {

        const Object &obj = objects[i];

        const unsigned char *color = colors[color_index % 19];

        color_index++;

        cv::Scalar cc(color[0], color[1], color[2]);

        fprintf(stderr, "%d = %.5f at %.2f %.2f %.2f x %.2f\n", obj.label, obj.prob, obj.rect.x,

                obj.rect.y, obj.rect.width, obj.rect.height);

        cv::rectangle(image, obj.rect, cc, 2);

        char text[256];

        sprintf(text, "%s %.1f%%", class_names[obj.label], obj.prob * 100);

        int baseLine = 0;

        cv::Size label_size = cv::getTextSize(text, cv::FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);

        int x = obj.rect.x;

        int y = obj.rect.y - label_size.height - baseLine;

        if (y < 0)

            y = 0;

        if (x + label_size.width > image.cols)

            x = image.cols - label_size.width;

        cv::rectangle(

            image, cv::Rect(cv::Point(x, y), cv::Size(label_size.width, label_size.height + baseLine)),

            cc, -1);

        cv::putText(image, text, cv::Point(x, y + label_size.height), cv::FONT_HERSHEY_SIMPLEX, 0.5,

                    cv::Scalar(255, 255, 255));

    }

    //  cv::imshow("image", image);

    bool success_img = cv::imwrite("output.jpg", image);

    if (!success_img)

    {

        std::cout << "failed to save" << std::endl;

    }

    else

    {

        std::cout << "save successfully" << std::endl;

    }

    cv::waitKey(0);

}

int main(int argc, char **argv)

{

    if (argc != 4)

    {

        fprintf(stderr, "Usage: %s [parampath] [modelpath] [imagepath]\n", argv[0]);

        return -1;

    }

    const char *parampath = argv[1];

    const char *modelpath = argv[2];

    const char *imagepath = argv[3];

    cv::Mat m = cv::imread(imagepath, 1);

    if (m.empty())

    {

        fprintf(stderr, "cv::imread %s failed\n", imagepath);

        return -1;

    }

    std::vector<Object> objects;

    detect_yolo11(parampath, modelpath, m, objects);

    draw_objects(m, objects);

    return 0;

}

②在当前文件夹下打开终端进行编译


arm-linux-gnueabihf-g++ -o yolo_detect yolo_detect.cpp \

-I/home/dasing/桌面/yolo/ncnn-master/src \

-I/home/dasing/桌面/yolo/ncnn-master/build/src \

-L/home/dasing/桌面/yolo/ncnn-master/build/src \

-lncnn -lpthread -ldl -std=c++11

注意这里的路径改成自己下载的位置,-I和-L后面跟的路径是我们之前编译ncnn时产出的头文件和库文件的路径,这样编译器才能找到 ncnn 的相关文件。我们还需要指定 opencv 的头文件和库文件的路径,这样编译器才能找到 opencv 的相关文件。我们还需要指定交叉编译器的路径,这样编译器才能使用交叉编译器来编译我们的程序。

在这里插入图片描述

编译后成功生成我们想要的文件。

③把编译好的可执行文件复制到开发板上

我们把编译好的可执行文件拖入sd卡,插入sd卡到开发

我们需要把四个文件放入sd卡,分别是编译好的可执行文件yolo_detect,还有之前下载的yolov11n.param和yolov11n.bin这两个模型文件,还有我们要测试的图片文件test.jpg。

我们在开发板上打开sd卡所在目录,执行命令:


//./可执行文件 模型param文件 模型bin文件 测试图片

./yolo_detect yolov11n.param yolov11n.bin test.jpg

如果运行成功,我们就可以在当前目录下看到一个output.jpg的文件,这个文件就是我们运行程序后生成的结果图片,里面包含了我们检测到的对象和它们的类别和置信度。

在这里插入图片描述

这里的报错只是是 OpenCV 的 GUI 显示功能没编译出来,所以无法显示图片,这个报错不影响我们后续的开发。我们可以直接在当前目录下找到 output.jpg 这个文件,这个文件就是我们运行程序后生成的结果图片,里面包含了我们检测到的对象和它们的类别和置信度。

在这里插入图片描述

1.2.5 图片格式转化

因为yolov11n的.param文件里输入的图片格式是640×640的,所以我们需要把测试的图片转化成640×640的输入尺寸。

也可以不转换,直接输入原图,但是如果输入原图的话,可能会影响检测的效果,因为模型是基于640×640的输入尺寸训练的,所以输入原图可能会导致检测的效果不佳,所以建议把测试的图片转化成640×640的输入尺寸,这样可以保证检测的效果。

用下面这个在线工具一键修改:

1.把原图保存到电脑。

2.打开这个免费工具:iloveimg.com/resize-image

3.上传图片 → 设置尺寸为 640×640 → 下载即可。

1.3 摄像头调用

v4l2摄像头,正点原子的ov5640摄像头,不要买成了ov2640,官方烧写的镜像里面没有ov2640的驱动,还得自己修改zimage和.dtb很麻烦,ov5640的驱动已经烧写在官方的镜像里了,直接插上摄像头就可以使用了。

我们可以使用 v4l2 的命令行工具来测试摄像头是否正常工作,命令如下:


v4l2-ctl --list-devices

如果摄像头正常工作,我们就可以看到摄像头的设备信息了,说明摄像头已经被系统识别了,可以正常使用了。

v4l2摄像头.cpp


/***************************************************************

 Copyright © ALIENTEK Co., Ltd. 1998-2021. All rights reserved.

 文件名 : v4l2_camera.c

 作者 : 邓涛

 版本 : V1.0

 描述 : V4L2摄像头应用编程实战

 其他 : 无

 论坛 : www.openedv.com

 日志 : 初版 V1.0 2021/7/09 邓涛创建

 ***************************************************************/

#include <stdio.h>

#include <stdlib.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <unistd.h>

#include <sys/ioctl.h>

#include <string.h>

#include <errno.h>

#include <sys/mman.h>

#include <linux/videodev2.h>

#include <linux/fb.h>

#define FB_DEV              "/dev/fb0"      //LCD设备节点

#define FRAMEBUFFER_COUNT   3               //帧缓冲数量

/*** 摄像头像素格式及其描述信息 ***/

typedef struct camera_format {

    unsigned char description[32];  //字符串描述信息

    unsigned int pixelformat;       //像素格式

} cam_fmt;

/*** 描述一个帧缓冲的信息 ***/

typedef struct cam_buf_info {

    unsigned short *start;      //帧缓冲起始地址

    unsigned long length;       //帧缓冲长度

} cam_buf_info;

static int width;                       //LCD宽度

static int height;                      //LCD高度

static unsigned short *screen_base = NULL;//LCD显存基地址

static int fb_fd = -1;                  //LCD设备文件描述符

static int v4l2_fd = -1;                //摄像头设备文件描述符

static cam_buf_info buf_infos[FRAMEBUFFER_COUNT];

static cam_fmt cam_fmts[10];

static int frm_width, frm_height;   //视频帧宽度和高度

static int fb_dev_init(void)

{

    struct fb_var_screeninfo fb_var = {0};

    struct fb_fix_screeninfo fb_fix = {0};

    unsigned long screen_size;

    /* 打开framebuffer设备 */

    fb_fd = open(FB_DEV, O_RDWR);

    if (0 > fb_fd) {

        fprintf(stderr, "open error: %s: %s\n", FB_DEV, strerror(errno));

        return -1;

    }

    /* 获取framebuffer设备信息 */

    ioctl(fb_fd, FBIOGET_VSCREENINFO, &fb_var);

    ioctl(fb_fd, FBIOGET_FSCREENINFO, &fb_fix);

    screen_size = fb_fix.line_length * fb_var.yres;

    width = fb_var.xres;

    height = fb_var.yres;

    /* 内存映射 */

    screen_base = mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);

    if (MAP_FAILED == (void *)screen_base) {

        perror("mmap error");

        close(fb_fd);

        return -1;

    }

    /* LCD背景刷白 */

    memset(screen_base, 0xFF, screen_size);

    return 0;

}

static int v4l2_dev_init(const char *device)

{

    struct v4l2_capability cap = {0};

    /* 打开摄像头 */

    v4l2_fd = open(device, O_RDWR);

    if (0 > v4l2_fd) {

        fprintf(stderr, "open error: %s: %s\n", device, strerror(errno));

        return -1;

    }

    /* 查询设备功能 */

    ioctl(v4l2_fd, VIDIOC_QUERYCAP, &cap);

    /* 判断是否是视频采集设备 */

    if (!(V4L2_CAP_VIDEO_CAPTURE & cap.capabilities)) {

        fprintf(stderr, "Error: %s: No capture video device!\n", device);

        close(v4l2_fd);

        return -1;

    }

    return 0;

}

static void v4l2_enum_formats(void)

{

    struct v4l2_fmtdesc fmtdesc = {0};

    /* 枚举摄像头所支持的所有像素格式以及描述信息 */

    fmtdesc.index = 0;

    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FMT, &fmtdesc)) {

        // 将枚举出来的格式以及描述信息存放在数组中

        cam_fmts[fmtdesc.index].pixelformat = fmtdesc.pixelformat;

        strcpy(cam_fmts[fmtdesc.index].description, fmtdesc.description);

        fmtdesc.index++;

    }

}

static void v4l2_print_formats(void)

{

    struct v4l2_frmsizeenum frmsize = {0};

    struct v4l2_frmivalenum frmival = {0};

    int i;

    frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    for (i = 0; cam_fmts[i].pixelformat; i++) {

        printf("format<0x%x>, description<%s>\n", cam_fmts[i].pixelformat,

                    cam_fmts[i].description);

        /* 枚举出摄像头所支持的所有视频采集分辨率 */

        frmsize.index = 0;

        frmsize.pixel_format = cam_fmts[i].pixelformat;

        frmival.pixel_format = cam_fmts[i].pixelformat;

        while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMESIZES, &frmsize)) {

            printf("size<%d*%d> ",

                    frmsize.discrete.width,

                    frmsize.discrete.height);

            frmsize.index++;

            /* 获取摄像头视频采集帧率 */

            frmival.index = 0;

            frmival.width = frmsize.discrete.width;

            frmival.height = frmsize.discrete.height;

            while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmival)) {

                printf("<%dfps>", frmival.discrete.denominator /

                        frmival.discrete.numerator);

                frmival.index++;

            }

            printf("\n");

        }

        printf("\n");

    }

}

static int v4l2_set_format(void)

{

    struct v4l2_format fmt = {0};

    struct v4l2_streamparm streamparm = {0};

    /* 设置帧格式 */

    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//type类型

    fmt.fmt.pix.width = width;  //视频帧宽度

    fmt.fmt.pix.height = height;//视频帧高度

    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;  //像素格式

    if (0 > ioctl(v4l2_fd, VIDIOC_S_FMT, &fmt)) {

        fprintf(stderr, "ioctl error: VIDIOC_S_FMT: %s\n", strerror(errno));

        return -1;

    }

    /*** 判断是否已经设置为我们要求的RGB565像素格式

    如果没有设置成功表示该设备不支持RGB565像素格式 */

    if (V4L2_PIX_FMT_RGB565 != fmt.fmt.pix.pixelformat) {

        fprintf(stderr, "Error: the device does not support RGB565 format!\n");

        return -1;

    }

    frm_width = fmt.fmt.pix.width;  //获取实际的帧宽度

    frm_height = fmt.fmt.pix.height;//获取实际的帧高度

    printf("视频帧大小<%d * %d>\n", frm_width, frm_height);

    /* 获取streamparm */

    streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    ioctl(v4l2_fd, VIDIOC_G_PARM, &streamparm);

    /** 判断是否支持帧率设置 **/

    if (V4L2_CAP_TIMEPERFRAME & streamparm.parm.capture.capability) {

        streamparm.parm.capture.timeperframe.numerator = 1;

        streamparm.parm.capture.timeperframe.denominator = 30;//30fps

        if (0 > ioctl(v4l2_fd, VIDIOC_S_PARM, &streamparm)) {

            fprintf(stderr, "ioctl error: VIDIOC_S_PARM: %s\n", strerror(errno));

            return -1;

        }

    }

    return 0;

}

static int v4l2_init_buffer(void)

{

    struct v4l2_requestbuffers reqbuf = {0};

    struct v4l2_buffer buf = {0};

    /* 申请帧缓冲 */

    reqbuf.count = FRAMEBUFFER_COUNT;       //帧缓冲的数量

    reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    reqbuf.memory = V4L2_MEMORY_MMAP;

    if (0 > ioctl(v4l2_fd, VIDIOC_REQBUFS, &reqbuf)) {

        fprintf(stderr, "ioctl error: VIDIOC_REQBUFS: %s\n", strerror(errno));

        return -1;

    }

    /* 建立内存映射 */

    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    buf.memory = V4L2_MEMORY_MMAP;

    for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) {

        ioctl(v4l2_fd, VIDIOC_QUERYBUF, &buf);

        buf_infos[buf.index].length = buf.length;

        buf_infos[buf.index].start = mmap(NULL, buf.length,

                PROT_READ | PROT_WRITE, MAP_SHARED,

                v4l2_fd, buf.m.offset);

        if (MAP_FAILED == buf_infos[buf.index].start) {

            perror("mmap error");

            return -1;

        }

    }

    /* 入队 */

    for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) {

        if (0 > ioctl(v4l2_fd, VIDIOC_QBUF, &buf)) {

            fprintf(stderr, "ioctl error: VIDIOC_QBUF: %s\n", strerror(errno));

            return -1;

        }

    }

    return 0;

}

static int v4l2_stream_on(void)

{

    /* 打开摄像头、摄像头开始采集数据 */

    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    if (0 > ioctl(v4l2_fd, VIDIOC_STREAMON, &type)) {

        fprintf(stderr, "ioctl error: VIDIOC_STREAMON: %s\n", strerror(errno));

        return -1;

    }

    return 0;

}

static void v4l2_read_data(void)

{

    struct v4l2_buffer buf = {0};

    unsigned short *base;

    unsigned short *start;

    int min_w, min_h;

    int j;

    if (width > frm_width)

        min_w = frm_width;

    else

        min_w = width;

    if (height > frm_height)

        min_h = frm_height;

    else

        min_h = height;

    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    buf.memory = V4L2_MEMORY_MMAP;

    for ( ; ; ) {

        for(buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) {

            ioctl(v4l2_fd, VIDIOC_DQBUF, &buf);     //出队

            for (j = 0, base=screen_base, start=buf_infos[buf.index].start;

                        j < min_h; j++) {

                memcpy(base, start, min_w * 2); //RGB565 一个像素占2个字节

                base += width;  //LCD显示指向下一行

                start += frm_width;//指向下一行数据

            }

            // 数据处理完之后、再入队、往复

            ioctl(v4l2_fd, VIDIOC_QBUF, &buf);

        }

    }

}

int main(int argc, char *argv[])

{

    if (2 != argc) {

        fprintf(stderr, "Usage: %s <video_dev>\n", argv[0]);

        exit(EXIT_FAILURE);

    }

    /* 初始化LCD */

    if (fb_dev_init())

        exit(EXIT_FAILURE);

    /* 初始化摄像头 */

    if (v4l2_dev_init(argv[1]))

        exit(EXIT_FAILURE);

    /* 枚举所有格式并打印摄像头支持的分辨率及帧率 */

    v4l2_enum_formats();

    v4l2_print_formats();

    /* 设置格式 */

    if (v4l2_set_format())

        exit(EXIT_FAILURE);

    /* 初始化帧缓冲:申请、内存映射、入队 */

    if (v4l2_init_buffer())

        exit(EXIT_FAILURE);

    /* 开启视频采集 */

    if (v4l2_stream_on())

        exit(EXIT_FAILURE);

    /* 读取数据:出队 */

    v4l2_read_data();       //在函数内循环采集数据、将其显示到LCD屏

    exit(EXIT_SUCCESS);

}

我们创建一个makefile,写一个makefile来编译这个摄像头调用的程序,makefile的内容如下:


# 交叉编译器(正点原子IMX6U)

CC = arm-linux-gnueabihf-gcc

# 编译选项:静态编译

CFLAGS = -Wall -g -static

# 你的源码文件(只有这一个)

SRC = v4l2_camera.c

# 要生成的可执行文件(只有一个)

TARGET = camera

# 编译

all:

    $(CC) $(CFLAGS) -o $(TARGET) $(SRC)

# 清理

clean:

    rm -f $(TARGET)

打开终端执行make,生成camera这个可执行文件,把它复制到开发板上,执行命令:


//这里的摄像头设备文件/dev/video0可能是video0,也可能是video1,两个都试一下。

./camera /dev/video0

在这里插入图片描述

1.4 摄像头实时识别LCD显示

我们把之前的yolo_detect.cpp和v4l2_camera.c这两个文件合并成一个文件,命名为detect.cpp,这样我们就可以在摄像头实时采集的同时进行目标检测了。
需要注意的是,我们yolo模型的尺寸是640×640的,所以我们需要把摄像头采集到的图像进行缩放,缩放到640×640的尺寸,这样才能保证检测的效果。然后我们还需要更改lcd屏幕显示的尺寸,因为我们缩放了图像,所以lcd屏幕显示的尺寸也需要更改成640×640的尺寸,这样才能保证显示的效果。
我们在yolo文件夹新建一个detect.cpp,内容如下:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <errno.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <linux/fb.h>
#include <signal.h>
#include <opencv2/opencv.hpp>
#include <vector>
#include <algorithm>
#include "net.h"
#include "layer.h"

#define FB_DEV              "/dev/fb0"
#define FRAMEBUFFER_COUNT   3
#define CAM_W               640
#define CAM_H               480
#define MAX_STRIDE          32

using namespace std;
using namespace cv;

static const char *class_names[] = {
    "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"
};

struct Object {
    cv::Rect_<float> rect;
    int label;
    float prob;
};

static inline float intersection_area(const Object &a, const Object &b) {
    cv::Rect_<float> inter = a.rect & b.rect;
    return inter.area();
}

static void qsort_descent_inplace(std::vector<Object> &objects, int left, int right) {
    int i = left;
    int j = right;
    float p = objects[(left + right) / 2].prob;

    while (i <= j) {
        while (objects[i].prob > p) i++;
        while (objects[j].prob < p) j--;
        if (i <= j) {
            std::swap(objects[i], objects[j]);
            i++; j--;
        }
    }

    #pragma omp parallel sections
    {
        #pragma omp section
        if (left < j) qsort_descent_inplace(objects, left, j);
        #pragma omp section
        if (i < right) qsort_descent_inplace(objects, i, right);
    }
}

static void qsort_descent_inplace(std::vector<Object> &objects) {
    if (objects.empty()) return;
    qsort_descent_inplace(objects, 0, objects.size() - 1);
}

static void nms_sorted_bboxes(const std::vector<Object> &faceobjects, std::vector<int> &picked,
                              float nms_threshold, bool agnostic = false) {
    picked.clear();
    int n = faceobjects.size();
    std::vector<float> areas(n);
    for (int i = 0; i < n; i++) areas[i] = faceobjects[i].rect.area();

    for (int i = 0; i < n; i++) {
        const Object &a = faceobjects[i];
        int keep = 1;
        for (int j = 0; j < (int)picked.size(); j++) {
            const Object &b = faceobjects[picked[j]];
            if (!agnostic && a.label != b.label) continue;

            float inter_area = intersection_area(a, b);
            float union_area = areas[i] + areas[picked[j]] - inter_area;
            if (inter_area / union_area > nms_threshold) keep = 0;
        }
        if (keep) picked.push_back(i);
    }
}

static inline float clampf(float d, float min, float max) {
    const float t = d < min ? min : d;
    return t > max ? max : t;
}

static void parse_yolo11_detections(float *inputs, float confidence_threshold, int num_channels,
                                    int num_anchors, int num_labels, int infer_img_width,
                                    int infer_img_height, std::vector<Object> &objects) {
    std::vector<Object> detections;
    cv::Mat output = cv::Mat((int)num_channels, (int)num_anchors, CV_32F, inputs).t();

    for (int i = 0; i < num_anchors; i++) {
        const float *row_ptr = output.row(i).ptr<float>();
        const float *bboxes_ptr = row_ptr;
        const float *scores_ptr = row_ptr + 4;
        const float *max_s_ptr = std::max_element(scores_ptr, scores_ptr + num_labels);
        float score = *max_s_ptr;

        if (score > confidence_threshold) {
            float x = *bboxes_ptr++;
            float y = *bboxes_ptr++;
            float w = *bboxes_ptr++;
            float h = *bboxes_ptr;

            float x0 = clampf((x - 0.5f * w), 0.f, (float)infer_img_width);
            float y0 = clampf((y - 0.5f * h), 0.f, (float)infer_img_height);
            float x1 = clampf((x + 0.5f * w), 0.f, (float)infer_img_width);
            float y1 = clampf((y + 0.5f * h), 0.f, (float)infer_img_height);

            Object obj;
            obj.rect.x = x0;
            obj.rect.y = y0;
            obj.rect.width = x1 - x0;
            obj.rect.height = y1 - y0;
            obj.label = max_s_ptr - scores_ptr;
            obj.prob = score;
            detections.push_back(obj);
        }
    }
    objects = detections;
}

//detect_yolo11 接收已加载好的 ncnn::Net 引用
static int detect_yolo11(ncnn::Net &yolo11, const cv::Mat &bgr, std::vector<Object> &objects) {
    const int target_size = 640;
    const float prob_threshold = 0.25f;
    const float nms_threshold = 0.45f;

    int img_w = bgr.cols;
    int img_h = bgr.rows;

    int w = img_w, h = img_h;
    float scale = 1.f;

    if (w > h) {
        scale = (float)target_size / w;
        w = target_size;
        h = h * scale;
    } else {
        scale = (float)target_size / h;
        h = target_size;
        w = w * scale;
    }

    ncnn::Mat in = ncnn::Mat::from_pixels_resize(bgr.data, ncnn::Mat::PIXEL_BGR2RGB, img_w, img_h, w, h);
    int wpad = (target_size + MAX_STRIDE - 1) / MAX_STRIDE * MAX_STRIDE - w;
    int hpad = (target_size + MAX_STRIDE - 1) / MAX_STRIDE * MAX_STRIDE - h;

    ncnn::Mat in_pad;
    ncnn::copy_make_border(in, in_pad, hpad / 2, hpad - hpad / 2, wpad / 2, wpad - wpad / 2, ncnn::BORDER_CONSTANT, 114.f);

    const float norm_vals[3] = {1 / 255.f, 1 / 255.f, 1 / 255.f};
    in_pad.substract_mean_normalize(0, norm_vals);

    ncnn::Extractor ex = yolo11.create_extractor();
    ex.input("in0", in_pad);

    vector<Object> proposals;
    ncnn::Mat out;
    ex.extract("out0", out);

    int num_labels = sizeof(class_names) / sizeof(class_names[0]);
    vector<Object> objects32;
    parse_yolo11_detections((float *)out.data, prob_threshold, out.h, out.w, num_labels, in_pad.w, in_pad.h, objects32);
    proposals.insert(proposals.end(), objects32.begin(), objects32.end());

    qsort_descent_inplace(proposals);
    vector<int> picked;
    nms_sorted_bboxes(proposals, picked, nms_threshold);

    objects.resize(picked.size());
    for (int i = 0; i < picked.size(); i++) {
        objects[i] = proposals[picked[i]];
        float x0 = (objects[i].rect.x - wpad / 2) / scale;
        float y0 = (objects[i].rect.y - hpad / 2) / scale;
        float x1 = (objects[i].rect.x + objects[i].rect.width - wpad / 2) / scale;
        float y0f = (objects[i].rect.y + objects[i].rect.height - hpad / 2) / scale;

        x0 = max(min(x0, (float)img_w - 1), 0.f);
        y0 = max(min(y0, (float)img_h - 1), 0.f);
        x1 = max(min(x1, (float)img_w - 1), 0.f);
        y0f = max(min(y0f, (float)img_h - 1), 0.f);

        objects[i].rect.x = x0;
        objects[i].rect.y = y0;
        objects[i].rect.width = x1 - x0;
        objects[i].rect.height = y0f - y0;
    }

    return 0;
}

// LCD / 摄像头部分
struct cam_buf_info { unsigned short *start; unsigned long length; };
static int lcd_w, lcd_h;
static unsigned short *lcd_buf;
static int fb_fd, cam_fd;
static cam_buf_info buf_infos[FRAMEBUFFER_COUNT];
static volatile int stop = 0;

static void sig(int s) { stop = 1; }

static int lcd_init() {
    fb_fd = open(FB_DEV, O_RDWR);
    struct fb_var_screeninfo v;
    struct fb_fix_screeninfo f;
    ioctl(fb_fd, FBIOGET_VSCREENINFO, &v);
    ioctl(fb_fd, FBIOGET_FSCREENINFO, &f);
    lcd_w = v.xres;
    lcd_h = v.yres;
    lcd_buf = (unsigned short *)mmap(NULL, f.line_length * v.yres, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
    return 0;
}

static void lcd_rect(int x, int y, int w, int h, unsigned short c) {
    x = max(0, min(x, lcd_w - 1));
    y = max(0, min(y, lcd_h - 1));
    w = max(0, min(w, lcd_w - x));
    h = max(0, min(h, lcd_h - y));
    for (int i = x; i < x + w; i++) lcd_buf[y * lcd_w + i] = c;
    for (int i = x; i < x + w; i++) lcd_buf[(y + h - 1) * lcd_w + i] = c;
    for (int j = y; j < y + h; j++) lcd_buf[j * lcd_w + x] = c;
    for (int j = y; j < y + h; j++) lcd_buf[j * lcd_w + (x + w - 1)] = c;
}

static int cam_init(const char *dev) {
    cam_fd = open(dev, O_RDWR);
    struct v4l2_format fmt = {0};
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = CAM_W;
    fmt.fmt.pix.height = CAM_H;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
    ioctl(cam_fd, VIDIOC_S_FMT, &fmt);

    struct v4l2_requestbuffers req = {0};
    req.count = FRAMEBUFFER_COUNT;
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;
    ioctl(cam_fd, VIDIOC_REQBUFS, &req);

    for (int i = 0; i < FRAMEBUFFER_COUNT; i++) {
        struct v4l2_buffer b = {0};
        b.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        b.memory = V4L2_MEMORY_MMAP;
        b.index = i;
        ioctl(cam_fd, VIDIOC_QUERYBUF, &b);
        buf_infos[i].start = (unsigned short *)mmap(NULL, b.length, PROT_READ | PROT_WRITE, MAP_SHARED, cam_fd, b.m.offset);
    }

    for (int i = 0; i < FRAMEBUFFER_COUNT; i++) {
        struct v4l2_buffer b = {0};
        b.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        b.memory = V4L2_MEMORY_MMAP;
        b.index = i;
        ioctl(cam_fd, VIDIOC_QBUF, &b);
    }

    enum v4l2_buf_type t = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(cam_fd, VIDIOC_STREAMON, &t);
    return 0;
}

int main(int argc, char **argv) {
    if (argc != 4) {
        printf("用法: %s /dev/video0 model.param model.bin\n", argv[0]);
        return -1;
    }

    signal(SIGINT, sig);
    lcd_init();
    cam_init(argv[1]);
    int x_off = (lcd_w - CAM_W) / 2;

    // 模型只加载一次
    ncnn::Net yolo11;
    yolo11.opt.use_vulkan_compute = false;
    yolo11.load_param(argv[2]);
    yolo11.load_model(argv[3]);

    Mat frame(CAM_H, CAM_W, CV_8UC3);
    vector<Object> objs;

    while (!stop) {
        struct v4l2_buffer b = {0};
        b.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        b.memory = V4L2_MEMORY_MMAP;
        ioctl(cam_fd, VIDIOC_DQBUF, &b);
        unsigned short *src = buf_infos[b.index].start;

        // 显示到 LCD
        for (int y = 0; y < CAM_H; y++)
            memcpy(lcd_buf + y * lcd_w + x_off, src + y * CAM_W, CAM_W * 2);

        // RGB565 → BGR
        for (int y = 0; y < CAM_H; y++) {
            for (int x = 0; x < CAM_W; x++) {
                unsigned short c = src[y * CAM_W + x];
                unsigned char r = ((c >> 11) & 0x1F) << 3;
                unsigned char g = ((c >> 5) & 0x3F) << 2;
                unsigned char b = (c & 0x1F) << 3;
                frame.at<Vec3b>(y, x) = Vec3b(b, g, r);
            }
        }

        // 调用 detect_yolo11(传入已加载好的模型)
        detect_yolo11(yolo11, frame, objs);

        // 画框
        for (auto &o : objs) {
            int x = o.rect.x + x_off;
            int y = o.rect.y;
            int w = o.rect.width;
            int h = o.rect.height;
            lcd_rect(x, y, w, h, 0xF800);
            printf("%d %.1f%% | %s\n", o.label, o.prob * 100, class_names[o.label]);
        }

        ioctl(cam_fd, VIDIOC_QBUF, &b);
    }

    // 程序退出时统一清理
    yolo11.clear();
    enum v4l2_buf_type t = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(cam_fd, VIDIOC_STREAMOFF, &t);
    close(cam_fd);
    munmap(lcd_buf, lcd_w * lcd_h * 2);
    close(fb_fd);
    return 0;
}

然后我们进入终端进行编译,大家根据自己的路径来修改,主要是要把ncnn的头文件和库文件的路径添加到编译命令里,编译命令如下:

arm-linux-gnueabihf-g++ -o detect detect.cpp 
-I/home/dasing/桌面/yolo/ncnn-master/src 
-I/home/dasing/桌面/yolo/ncnn-master/build/src 
-I/opt/opencv-arm/include 
-L/opt/opencv-arm/lib /home/dasing/桌面/yolo/ncnn-master/build/src/libncnn.a 
-lopencv_core -lopencv_imgproc -lopencv_highgui -lopencv_imgcodecs -lpthread -ldl -std=c++11

编译成功后,我们把生成的detect这个可执行文件复制到开发板上,执行命令:

./detect /dev/video1 yolov11n.param yolov11n.bin

在这里插入图片描述

这个是识别的编号置信度和对应的类型
在这里插入图片描述

对应图中的框框出了person,chair,bottle这些对象,所以我们可以看到这个模型的检测效果还是非常不错的,能够准确地检测出图中的对象,并且给出对应的类别和置信度。
就是只有cpu而且还要同时执行摄像头采集数据和目标检测还要显示到lcd屏幕上,所以帧率可能会比较低,大家可以根据自己的需求来调整检测的频率,比如说每隔几帧进行一次检测,这样可以提高整体的帧率,同时也能保证检测的效果。

1.5 中间出现的问题解决

1.5.1 模型输入尺寸不对导致检测效果不佳

之前我们在进行目标检测的时候,输入的图像尺寸没有统一成模型训练时的尺寸,导致检测效果不佳,甚至有时候会漏检或者误检,所以建议大家在进行目标检测之前,一定要把输入的图像尺寸统一成模型训练时的尺寸,这样才能保证检测的效果。
针对之前格式没统一导致的检测效果不佳的问题,比如像如下识别,就是识别到了人和猪,但是由于输入的图像尺寸不对,所以检测框的位置和大小都不太准确,甚至有时候会漏检或者误检,所以建议大家在进行目标检测之前,一定要把输入的图像尺寸统一成模型训练时的尺寸,这样才能保证检测的效果。
在这里插入图片描述

还有就是识别出来一堆框的情况如下图,可能是置信度太低或者YOLOv11 的 NMS(非极大值抑制)没有生效,模型输出的所有候选框都被画了出来,所以画面里全是密密麻麻的红框。需要提高置信度阈值,先过滤掉低置信度的无效框;调整 NMS 阈值,增强对重叠框的抑制。
在这里插入图片描述

在这里插入图片描述

1.5.2 没有将param和bin文件一块放入开发板导致无法运行

因为我们是动态加载模型文件,所以我们在进行目标检测的时候,模型的param和bin文件是必须要放在开发板上的,如果没有放在开发板上,那么就会导致无法运行,甚至有时候会出现编译错误。
在这里插入图片描述

这里就显示模型导入失败,没有找到模型,因为sd卡里面忘记拷贝了

1.5.3 代码,模型,环境不兼容导致无法编译或者运行

我们使用的代码是yolov11n模型进行目标检测的,要对应yolov11n的param和bin文件(因为网络结构不一样,所以不能使用其他模型),同时我们要查看我们自己模型的尺寸,我们也要查看自己摄像头的采集数据的尺寸,lcd的显示尺寸,调整合适的比例进行缩放或者裁剪。

1.5.4 摄像头

摄像头采集的尺寸得是640×480的,如果不是这个尺寸的,那么我们就需要进行缩放或者裁剪,调整成640×480的尺寸,这样才能保证检测的效果。
在这里插入图片描述

1.5.5 将opencv的库文件放入开发板的/usr/lib目录下

我们在进行编译的时候,一定要把opencv的库文件和头文件路径添加到编译命令里,否则就会导致编译错误,找不到opencv的相关函数和类。使用软链接的方式把opencv的库文件放入开发板的/usr/lib目录下,这样就可以在编译的时候直接链接到opencv的库文件了。
在这里插入图片描述

1.5.6 yolov11n.param定义的节点名要和代码里extract的节点名一致

我们在进行目标检测的时候,代码里extract的节点名要和yolov11n.param文件里定义的节点名一致,否则就会导致运行错误,找不到对应的节点,无法进行目标检测。
在这里插入图片描述

1.5.7 基于github上的开源代码删去gui部分(如果要自己要从源码开始修改)

因为开发板不是桌面版本的linux系统,所以我们在进行目标检测的时候,不能使用opencv的gui功能,比如说imshow, waitKey等函数,这些函数是用来在桌面环境下显示图像的,在开发板上是无法使用的,所以我们需要把这些函数删掉,或者改成其他的方式来显示图像,比如说直接把检测结果画到lcd屏幕上,这样就可以在开发板上进行目标检测了。
在这里插入图片描述

1.5.8 模型重复加载

出现pool allocator destroyed too early错误,是因为程序运行时,模型(ncnn::Net)被频繁创建和销毁,触发了 OpenCV 内存池的提前回收,与图像帧的生命周期冲突
解决方法:将模型加载移到主函数开头,只加载一次,不要在检测函数内部反复创建/销毁模型。模型在整个程序运行期间保持有效,直到程序退出时再统一释放。
在这里插入图片描述

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐