嵌入式linux岗位的核心考察点是 驱动开发、性能优化、资源调度、跨平台移植

核心技术栈:Linux V4L2驱动、GStreamer流媒体、轻量化模型部署、多线程性能优化

底层驱动与视频流采集

核心任务:

  • 基于 Linux V4L2 字符设备驱动框架,实现摄像头的参数配置(分辨率、帧率、像素格式如 YUYV)
  • 用mmap内存映射方式采集帧数据,避免 CPU 拷贝(对比 read 方式,降低延迟和 CPU 占用)
  • 编写 C/C++ 代码封装采集模块(嵌入式开发主流语言,简历上优先体现 C/C++ 能力)

视频流处理与低延迟优化

核心任务:

  • 用 GStreamer 搭建采集 - 编解码 - 显示的流水线,支持 H264 硬件编解码(树莓派的 GPU 加速)
  • 实现多线程分离:将 “视频采集”“算法推理”“结果显示” 拆分为 3 个独立线程(用 pthread 库),避免单线程阻塞
  • 监控系统资源:用top/ps命令统计 CPU / 内存占用率,目标是 1080P 分辨率下 CPU 占用低于 40%

轻量化算法部署

核心任务

  • 部署 YOLOv8n(nano 版)目标检测模型
  • 对模型进行 int8 量化(用 TensorRT 或 ONNX Runtime),降低算力消耗
  • 用 C++ 调用 TensorRT 推理引擎(避免 Python 的性能损耗)

工程化封装

  • 用 Makefile/CMake 管理项目编译(指定交叉编译工具链,为后续移植做准备)
  • 编写 Shell 脚本 实现一键启动(自动配置摄像头参数、加载模型、启动程序)
  • 处理异常情况:添加摄像头断开重连、模型加载失败的容错逻辑

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

在这里插入图片描述

阶段1:系统烧录与初始化

1.1 烧录64位Lite系统

在这里插入图片描述

1.2 SSH登录树莓派

在这里插入图片描述

1.3 系统级内存优化

# 1. 扩容SD卡(首次启动必做,避免空间不足)
sudo raspi-config --expand-rootfs

# 2. 调整交换分区(2GB内存需设4GB,避免OOM)
sudo fallocate -l 4G /swapfile  # 分配4GB交换空间
sudo chmod 600 /swapfile        # 权限限制
sudo mkswap /swapfile           # 格式化交换分区
sudo swapon /swapfile           # 启用交换分区
# 永久生效(写入fstab)
echo '/swapfile swap swap defaults 0 0' | sudo tee -a /etc/fstab

# 3. 降低GPU显存(释放内存给CPU)
sudo vim /boot/config.txt
# 找到/添加以下行(按i编辑,Esc后:wq保存):
gpu_mem=64  # 从默认128MB降至64MB(仅保留基础GPU编解码)
dtoverlay=vc4-fkms-v3d
disable_overscan=1

# 4. 优化内存调度(避免频繁换页)
sudo vim /etc/sysctl.conf
# 添加以下行:
vm.swappiness=10  # 优先用物理内存,仅当内存不足10%时用swap
vm.min_free_kbytes=10240  # 保留10MB空闲内存,避免卡死
# 生效配置
sudo sysctl -p

# 5. 关闭无用服务(释放内存/CPU)
sudo systemctl stop bluetooth avahi-daemon
sudo systemctl disable bluetooth avahi-daemon

# 6. 更新系统(国内源后续改,先基础更新)
sudo apt update && sudo apt upgrade -y
# 重启生效所有配置
sudo reboot

1.4 替换国内镜像源

# 1. 备份原源文件
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
sudo cp /etc/apt/sources.list.d/raspi.list /etc/apt/sources.list.d/raspi.list.bak

# 2. 修改系统源(适配64位bullseye/bookworm,优先bullseye)
sudo vim /etc/apt/sources.list
# 删除所有内容,粘贴以下(bullseye版本):
deb http://mirrors.tuna.tsinghua.edu.cn/raspbian/raspbian/ bullseye main non-free contrib rpi
deb-src http://mirrors.tuna.tsinghua.edu.cn/raspbian/raspbian/ bullseye main non-free contrib rpi

# 3. 修改扩展源
sudo vim /etc/apt/sources.list.d/raspi.list
# 删除所有内容,粘贴以下:
deb http://mirrors.tuna.tsinghua.edu.cn/raspberrypi/ bullseye main ui

# 4. 更新源(验证速度)
sudo apt update

1.5 配置pip国内源

# 创建pip配置目录
mkdir -p ~/.config/pip
# 编辑配置文件
vim ~/.config/pip/pip.conf
# 粘贴以下内容:
[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
[install]
trusted-host = pypi.tuna.tsinghua.edu.cn

阶段2:安装核心依赖

2.1 安装基础编译工具

sudo apt install -y build-essential cmake git vim pkg-config libv4l-dev v4l-utils --no-install-recommends

验证:执行gcc -v ,显示版本号。

2.2 安装GStreamer

sudo apt install -y gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good --no-install-recommends

验证:执行gst-launch-1.0 --version,显示版本号。

2.3 安装OpenCV

# 1. 安装OpenCV必需依赖(仅保留核心)
sudo apt install -y libjpeg-dev libpng-dev libavcodec-dev libavformat-dev libswscale-dev --no-install-recommends

# 2. 下载OpenCV 4.8.0(精简版,仅核心模块)
git clone --depth 1 --branch 4.8.0 https://github.com/opencv/opencv.git
cd opencv && mkdir build && cd build

# 3. CMake配置(关闭所有非核心模块,减少内存占用)
cmake -D CMAKE_BUILD_TYPE=RELEASE \
-D CMAKE_INSTALL_PREFIX=/usr/local \
-D ENABLE_NEON=ON \
-D ENABLE_VFPV3=ON \
-D BUILD_TESTS=OFF \
-D BUILD_EXAMPLES=OFF \
-D WITH_GSTREAMER=ON \
-D WITH_FFMPEG=ON \
-D WITH_OPENMP=OFF \  # 关闭OpenMP,减少线程内存占用
-D WITH_GTK=OFF \      # 关闭图形界面,无需显示依赖
-D BUILD_opencv_python=OFF \  # 关闭Python绑定,节省内存
..

# 4. 编译(2GB内存用2线程,避免OOM)
make -j2  # 约40分钟,耐心等待,不要中断

# 5. 安装
sudo make install
cd ~  # 返回主目录

验证:执行pkg-config --modversion opencv4,显示版本号。
在这里插入图片描述

cmake -D CMAKE_BUILD_TYPE=RELEASE \
-D CMAKE_INSTALL_PREFIX=/usr/local \
-D ENABLE_NEON=ON \          # 树莓派4B硬件加速(保留)
-D BUILD_TESTS=OFF \         # 关闭测试
-D BUILD_PERF_TESTS=OFF \    # 关闭性能测试
-D BUILD_EXAMPLES=OFF \      # 关闭示例
-D BUILD_DOCS=OFF \          # 关闭文档
-D BUILD_opencv_python=OFF \ # 关闭Python绑定
-D BUILD_opencv_java=OFF \   # 关闭Java绑定
-D BUILD_opencv_world=OFF \  # 关闭全模块打包
-D BUILD_opencv_dnn=OFF \    # 关闭DNN(深度学习)
-D BUILD_opencv_gapi=OFF \   # 关闭GAPI
-D BUILD_opencv_stitching=OFF \ # 关闭拼接
-D BUILD_opencv_photo=OFF \  # 关闭照片处理
-D BUILD_opencv_ml=OFF \     # 关闭机器学习
-D BUILD_opencv_flann=OFF \  # 关闭近邻搜索
-D BUILD_opencv_features2d=OFF \ # 关闭特征提取
-D BUILD_opencv_calib3d=OFF \ # 关闭相机标定
-D BUILD_opencv_ts=OFF \     # 关闭测试套件
-D WITH_GSTREAMER=ON \       # 保留(摄像头/视频流依赖)
-D WITH_FFMPEG=ON \          # 保留(视频解码依赖)
-D WITH_OPENMP=OFF \         # 关闭多线程(减少内存占用)
-D WITH_GTK=OFF \            # 关闭图形界面依赖
-D WITH_V4L=ON \             # 强制开启V4L(树莓派摄像头驱动,关键!)
..
// test_dnn.cpp
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>  // 确保包含dnn头文件
#include <iostream>

int main() {
    // 1. 验证OpenCV版本(基础检查)
    std::cout << "OpenCV version: " << CV_VERSION << std::endl;

    // 2. 验证dnn模块是否可用(核心:尝试创建dnn::Net对象)
    bool dnn_available = false;
    try {
        // 方式1:创建空的dnn::Net(不加载模型,仅验证模块是否链接)
        cv::dnn::Net net;
        dnn_available = true;

        // 方式2(可选):尝试加载一个无效的ONNX模型(验证readNetFromONNX接口)
        // 即使模型不存在,只要接口能调用,就说明dnn模块正常
        try {
            net = cv::dnn::readNetFromONNX("dummy.onnx");
            std::cout << "DNN readNetFromONNX接口调用成功(模型不存在属于正常)" << std::endl;
        } catch (const cv::Exception& e) {
            // 捕获“模型不存在”的异常,仅提示,不影响验证
            std::cout << "DNN readNetFromONNX接口可用(异常:" << e.what() << ")" << std::endl;
        }
    } catch (const std::exception& e) {
        std::cout << "DNN模块不可用,异常:" << e.what() << std::endl;
    }

    // 输出dnn模块状态
    std::cout << "DNN module available: " << (dnn_available ? "Yes" : "No") << std::endl;

    return 0;
}

2.4 安装TensorRT

# 1. 下载TensorRT 8.6 aarch64版本(适配树莓派64位)
wget https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/secure/8.6.1/local_repos/nv-tensorrt-local-repo-raspbian11-8.6.1-cuda-11.8_1.0-1_arm64.deb

# 2. 安装
sudo dpkg -i nv-tensorrt-local-repo-raspbian11-8.6.1-cuda-11.8_1.0-1_arm64.deb
sudo cp /var/nv-tensorrt-local-repo-raspbian11-8.6.1-cuda-11.8/*.gpg /usr/share/keyrings/
sudo apt update
sudo apt install -y tensorrt --no-install-recommends

验证:dpkg -l | grep tensorrt,显示安装成功。

阶段3:CSI摄像头调试(720p适配)

10bit Bayer GBGB/RGRG (pGAA/gb10)格式CSI摄像头。
核心目标:验证拜尔格式摄像头的硬件识别、分辨率支持,完成基础采集测试(无画面偏色、无内存溢出)。

3.1 前置准备:确认系统驱动 (适配Bookworm/Bullseye)

# 启用树莓派摄像头驱动(通用方案)
sudo nano /boot/firmware/config.txt
# 在文件末尾添加以下内容(启用CSI摄像头+降低GPU内存占用,省内存)
start_x=1
gpu_mem=128  # 仅分配128MB GPU内存,剩余给CPU/推理
# 保存退出:Ctrl+O→回车→Ctrl+X
sudo reboot  # 重启生效

3.2 验证摄像头硬件识别

# 1. 检查V4L2设备是否存在
ls /dev/video0  # 输出/dev/video0说明识别成功
# 2. 查看摄像头详细信息(确认拜耳格式)
v4l2-ctl --all -d /dev/video0 | grep "Pixel Format"
# 3. 列出拜耳格式支持的所有分辨率(关键!)
v4l2-ctl --list-formats-ext -d /dev/video0 | grep -A10 "GB10"

验证结果:正常输出如下:

[1]: 'GB10' (10-bit Bayer GBGB/RGRG)
    Size: Discrete 640x480
        Interval: Discrete 0.017s (60.000 fps)
    Size: Discrete 1280x720
        Interval: Discrete 0.033s (30.000 fps)

若无720p分辨率,仅用640*480即可。

3.3 测试拜尔格式采集

# 优先测试640x480(省内存)
v4l2-ctl --set-fmt-video=width=640,height=480,pixelformat=GB10 --stream-mmap --stream-count=100 --stream-to=/dev/null
# 可选:测试720P(若内存充足)
v4l2-ctl --set-fmt-video=width=1280,height=720,pixelformat=GB10 --stream-mmap --stream-count=50 --stream-to=/dev/null

验证结果
正常输出:无「pixelformat invalid」报错,显示帧率(如 29.8fps);
兜底方案:若 720P 采集卡顿 / 报错,放弃 720P,全程用 640x480。

3.4 安装拜尔转换依赖(OpenCV补充)

# 确保OpenCV的imgproc模块完整(处理拜耳转换)
sudo apt install -y libopencv-imgproc-dev --no-install-recommends

阶段4:V4L2视频采集模块开发

核心目标:开发精简版V4L2采集代码,实现 [拜尔 10bit->8bit->BGR]转换,640*480分辨率,内存占用<=500MB。

4.1 完整V4L2采集代码

创建文件:v4l2_bayer_capture.cpp

// v4l2_bayer_capture.cpp
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <opencv2/opencv.hpp>
#include <errno.h>
#include <string>
#include <cstdio>
#include <signal.h>
#include <vector>
#include <atomic>

// ===================== 全局配置(可根据需求修改) =====================
#define CAMERA_DEVICE "/dev/video0"    // 摄像头设备路径
#define CAPTURE_WIDTH 640              // 采集宽度
#define CAPTURE_HEIGHT 480             // 采集高度
#define EXPOSURE_VALUE 1200             // 手动曝光值(越大越亮,需匹配摄像头范围)
#define GAIN_VALUE 40                  // 增益值(越大越亮,需匹配摄像头范围)
#define BRIGHTNESS_VALUE 255           // 亮度值(0-255,默认128)
#define CONTRAST_VALUE 150             // 对比度值(0-255,默认128)
#define SOFTWARE_BRIGHT_ALPHA 1.5      // 软件亮度增益(1.0=原亮度,>1增亮)
#define SOFTWARE_BRIGHT_BETA 30        // 软件亮度偏移(额外增亮值)
// =====================================================================

// 全局退出标志(原子变量,避免多线程竞争)
std::atomic<bool> g_quit(false);
// 缓冲区结构体(存储映射的内存地址和长度)
struct Buffer {
    void* start;
    size_t length;
    int index;
};
// 缓冲区数组(动态适配摄像头驱动分配的数量)
std::vector<Buffer> buffers;

int fd = -1;                // 摄像头设备文件描述符
int actual_buf_count = 0;   // 驱动实际分配的缓冲区数量

/**
 * @brief 安全释放摄像头资源(缓冲区、文件描述符)
 */
void safe_release() {
    if (fd >= 0) {
        // 停止视频流
        enum v4l2_buf_type stream_type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        ioctl(fd, VIDIOC_STREAMOFF, &stream_type);
        
        // 释放所有映射的缓冲区
        for (auto& buf : buffers) {
            if (buf.start != MAP_FAILED && buf.start != nullptr) {
                munmap(buf.start, buf.length);
                buf.start = nullptr;
            }
        }
        buffers.clear();
        
        // 关闭设备
        close(fd);
        fd = -1;
        std::cout << "[INFO] 摄像头资源已安全释放" << std::endl;
    }
}

/**
 * @brief 信号处理函数(捕获Ctrl+C,优雅退出)
 */
void sigint_handler(int signum) {
    std::cout << "\n[INFO] 捕获到Ctrl+C信号(" << signum << "),准备退出..." << std::endl;
    g_quit = true;
}

/**
 * @brief 设置摄像头硬件参数(曝光/增益/亮度/对比度)
 * @return true-设置成功(部分参数不支持也返回true),false-设备未打开
 */
bool set_camera_hw_params() {
    if (fd < 0) {
        std::cerr << "[ERROR] 摄像头未打开,无法设置硬件参数" << std::endl;
        return false;
    }

    // -------------------- 1. 关闭自动曝光(必须先关,才能设手动曝光) --------------------
    struct v4l2_control ctrl_auto_exposure = {0};
    ctrl_auto_exposure.id = V4L2_CID_EXPOSURE_AUTO;
    ctrl_auto_exposure.value = V4L2_EXPOSURE_MANUAL; // 手动曝光模式
    if (ioctl(fd, VIDIOC_S_CTRL, &ctrl_auto_exposure) < 0) {
        std::cerr << "[WARNING] 摄像头不支持手动曝光模式(忽略,继续设置其他参数):" << strerror(errno) << std::endl;
    } else {
        std::cout << "[INFO] 已关闭自动曝光,切换为手动模式" << std::endl;

        // 设置手动曝光值(仅当手动曝光模式开启时有效)
        struct v4l2_control ctrl_exposure = {0};
        ctrl_exposure.id = V4L2_CID_EXPOSURE_ABSOLUTE;
        ctrl_exposure.value = EXPOSURE_VALUE;
        if (ioctl(fd, VIDIOC_S_CTRL, &ctrl_exposure) < 0) {
            std::cerr << "[WARNING] 摄像头不支持设置手动曝光值:" << strerror(errno) << std::endl;
        } else {
            std::cout << "[INFO] 手动曝光值已设为:" << EXPOSURE_VALUE << std::endl;
        }
    }

    // -------------------- 2. 设置增益(增大进光量,提升亮度) --------------------
    struct v4l2_control ctrl_gain = {0};
    ctrl_gain.id = V4L2_CID_GAIN;
    ctrl_gain.value = GAIN_VALUE;
    if (ioctl(fd, VIDIOC_S_CTRL, &ctrl_gain) < 0) {
        std::cerr << "[WARNING] 摄像头不支持设置增益:" << strerror(errno) << std::endl;
    } else {
        std::cout << "[INFO] 增益值已设为:" << GAIN_VALUE << std::endl;
    }

    // -------------------- 3. 设置亮度(0-255,越大越亮) --------------------
    struct v4l2_control ctrl_brightness = {0};
    ctrl_brightness.id = V4L2_CID_BRIGHTNESS;
    ctrl_brightness.value = BRIGHTNESS_VALUE;
    if (ioctl(fd, VIDIOC_S_CTRL, &ctrl_brightness) < 0) {
        std::cerr << "[WARNING] 摄像头不支持设置亮度:" << strerror(errno) << std::endl;
    } else {
        std::cout << "[INFO] 亮度值已设为:" << BRIGHTNESS_VALUE << std::endl;
    }

    // -------------------- 4. 设置对比度(0-255,越大对比越强) --------------------
    struct v4l2_control ctrl_contrast = {0};
    ctrl_contrast.id = V4L2_CID_CONTRAST;
    ctrl_contrast.value = CONTRAST_VALUE;
    if (ioctl(fd, VIDIOC_S_CTRL, &ctrl_contrast) < 0) {
        std::cerr << "[WARNING] 摄像头不支持设置对比度:" << strerror(errno) << std::endl;
    } else {
        std::cout << "[INFO] 对比度值已设为:" << CONTRAST_VALUE << std::endl;
    }

    return true;
}

/**
 * @brief 初始化V4L2摄像头(适配GB10 10-bit GBRG格式)
 * @return true-初始化成功,false-失败
 */
bool init_v4l2_camera() {
    // -------------------- 1. 打开摄像头(阻塞模式,避免频繁出队失败) --------------------
    fd = open(CAMERA_DEVICE, O_RDWR, 0);
    if (fd < 0) {
        std::cerr << "[ERROR] 打开摄像头失败:" << strerror(errno) << std::endl;
        return false;
    }
    std::cout << "[INFO] 摄像头打开成功(fd=" << fd << ")" << std::endl;

    // -------------------- 2. 设置视频格式(GB10 10-bit GBRG) --------------------
    struct v4l2_format fmt = {0};
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = CAPTURE_WIDTH;
    fmt.fmt.pix.height = CAPTURE_HEIGHT;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_SGBRG10; // 匹配GB10格式(GBGB/RGRG)
    fmt.fmt.pix.field = V4L2_FIELD_NONE;
    if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
        std::cerr << "[ERROR] 设置GB10格式失败:" << strerror(errno) << std::endl;
        safe_release();
        return false;
    }
    // 验证实际设置的分辨率(摄像头可能自动调整)
    std::cout << "[INFO] 视频格式设置成功(GB10 10-bit),分辨率:" 
              << fmt.fmt.pix.width << "×" << fmt.fmt.pix.height << std::endl;

    // -------------------- 3. 请求缓冲区(驱动自动分配数量) --------------------
    struct v4l2_requestbuffers req = {0};
    req.count = 4;                // 请求4个缓冲区(拜耳摄像头推荐≥3)
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP; // 内存映射模式
    if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
        std::cerr << "[ERROR] 请求缓冲区失败:" << strerror(errno) << std::endl;
        safe_release();
        return false;
    }
    actual_buf_count = req.count;
    std::cout << "[INFO] 缓冲区请求成功,驱动分配数量:" << actual_buf_count << std::endl;

    // -------------------- 4. 映射所有缓冲区到用户空间 --------------------
    buffers.resize(actual_buf_count);
    for (int i = 0; i < actual_buf_count; i++) {
        struct v4l2_buffer buf = {0};
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = i;

        // 查询缓冲区信息
        if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
            std::cerr << "[ERROR] 查询缓冲区" << i << "失败:" << strerror(errno) << std::endl;
            safe_release();
            return false;
        }

        // 验证10-bit缓冲区长度(640×480×2=614400)
        if (buf.length != CAPTURE_WIDTH * CAPTURE_HEIGHT * 2) {
            std::cerr << "[ERROR] 缓冲区" << i << "长度不匹配(10-bit应为" 
                      << CAPTURE_WIDTH * CAPTURE_HEIGHT * 2 << ",实际:" << buf.length << ")" << std::endl;
            safe_release();
            return false;
        }

        // 内存映射
        buffers[i].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
        if (buffers[i].start == MAP_FAILED) {
            std::cerr << "[ERROR] 映射缓冲区" << i << "失败:" << strerror(errno) << std::endl;
            safe_release();
            return false;
        }

        // 保存缓冲区信息
        buffers[i].length = buf.length;
        buffers[i].index = i;
        std::cout << "[INFO] 缓冲区" << i << "映射成功(长度:" << buf.length << ")" << std::endl;

        // 缓冲区入队(准备接收数据)
        if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
            std::cerr << "[ERROR] 缓冲区" << i << "入队失败:" << strerror(errno) << std::endl;
            safe_release();
            return false;
        }
    }

    // -------------------- 5. 启动视频流 --------------------
    enum v4l2_buf_type stream_type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (ioctl(fd, VIDIOC_STREAMON, &stream_type) < 0) {
        std::cerr << "[ERROR] 启动视频流失败:" << strerror(errno) << std::endl;
        safe_release();
        return false;
    }
    std::cout << "[INFO] 视频流启动成功,预热500ms..." << std::endl;
    usleep(500000); // 拜耳摄像头预热,确保数据稳定

    // -------------------- 6. 设置硬件参数(曝光/增益/亮度) --------------------
    set_camera_hw_params();

    return true;
}

/**
 * @brief 采集一帧图像(拜耳转换+软件亮度补偿)
 * @return 处理后的BGR图像(空Mat表示采集失败)
 */
cv::Mat capture_frame() {
    if (fd < 0 || buffers.empty()) {
        std::cerr << "[ERROR] 摄像头未初始化,采集失败" << std::endl;
        return cv::Mat();
    }

    struct v4l2_buffer buf = {0};
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;

    // -------------------- 出队缓冲区(最多重试30次,间隔10ms) --------------------
    int retry = 30;
    int ret = -1;
    while (retry-- > 0) {
        ret = ioctl(fd, VIDIOC_DQBUF, &buf);
        if (ret >= 0) break;
        if (errno == EAGAIN) {
            usleep(10000); // 缓冲区未就绪,等待10ms重试
            continue;
        }
        break;
    }
    if (ret < 0) {
        std::cerr << "[ERROR] 缓冲区出队失败(重试30次):" << strerror(errno) << std::endl;
        return cv::Mat();
    }

    // -------------------- 验证缓冲区有效性 --------------------
    if (buf.index < 0 || buf.index >= actual_buf_count) {
        std::cerr << "[ERROR] 无效的缓冲区索引:" << buf.index << std::endl;
        ioctl(fd, VIDIOC_QBUF, &buf); // 重新入队
        return cv::Mat();
    }
    if (buf.length != buffers[buf.index].length) {
        std::cerr << "[ERROR] 缓冲区长度不匹配" << std::endl;
        ioctl(fd, VIDIOC_QBUF, &buf); // 重新入队
        return cv::Mat();
    }

    // -------------------- 10-bit拜耳转8-bit --------------------
    // 读取10-bit GBRG数据(2字节/像素)
    cv::Mat bayer_10bit(CAPTURE_HEIGHT, CAPTURE_WIDTH, CV_16UC1, buffers[buf.index].start);
    if (bayer_10bit.empty() || !bayer_10bit.data) {
        std::cerr << "[ERROR] 10-bit拜耳数据无效" << std::endl;
        ioctl(fd, VIDIOC_QBUF, &buf); // 重新入队
        return cv::Mat();
    }
    // 10-bit→8-bit缩放(10bit最大值1023 → 8bit最大值255)
    cv::Mat bayer_8bit;
    bayer_10bit.convertTo(bayer_8bit, CV_8UC1, 255.0 / 1023.0);

    // -------------------- 拜耳GBRG转BGR --------------------
    cv::Mat bgr_frame;
    cv::cvtColor(bayer_8bit, bgr_frame, cv::COLOR_BayerGB2BGR); // 匹配GB10格式的唯一标志
    if (bgr_frame.empty()) {
        std::cerr << "[ERROR] 拜耳转BGR失败" << std::endl;
        ioctl(fd, VIDIOC_QBUF, &buf); // 重新入队
        return cv::Mat();
    }

    // -------------------- 软件亮度补偿(补充硬件调节的不足) --------------------
    cv::Mat bright_frame;
    bgr_frame.convertTo(bright_frame, -1, SOFTWARE_BRIGHT_ALPHA, SOFTWARE_BRIGHT_BETA);

    // -------------------- 缓冲区重新入队(必须!否则后续无缓冲区可用) --------------------
    if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
        std::cerr << "[WARNING] 缓冲区重新入队失败:" << strerror(errno) << std::endl;
    }

    return bright_frame;
}

int main() {
    // 注册Ctrl+C信号处理函数
    signal(SIGINT, sigint_handler);
    std::cout << "[INFO] Ctrl+C信号处理器已注册(按Ctrl+C退出)" << std::endl;

    // 初始化摄像头
    if (!init_v4l2_camera()) {
        std::cerr << "[FATAL] 摄像头初始化失败,程序退出" << std::endl;
        return -1;
    }

    std::cout << "[INFO] 开始采集图像(按Ctrl+C停止)..." << std::endl;
    int frame_count = 0;
    int empty_frame_count = 0;

    while (!g_quit) {
        // 采集一帧
        cv::Mat frame = capture_frame();

        // 连续15次空帧,主动退出(避免死循环)
        if (frame.empty()) {
            empty_frame_count++;
            std::cerr << "[WARNING] 采集到空帧(" << empty_frame_count << "/15)" << std::endl;
            if (empty_frame_count >= 15) {
                std::cerr << "[FATAL] 连续15次采集空帧,程序退出" << std::endl;
                break;
            }
            usleep(20000);
            continue;
        }
        empty_frame_count = 0; // 重置空帧计数

        // 每5帧保存一张图片
        if (frame_count % 5 == 0) {
            std::string filename = "gb10_bright_frame_" + std::to_string(frame_count) + ".jpg";
            if (cv::imwrite(filename, frame)) {
                std::cout << "[INFO] 图像保存成功:" << filename << std::endl;
            } else {
                std::cerr << "[ERROR] 图像保存失败:" << filename << std::endl;
            }
        }

        frame_count++;
        usleep(10000); // 降低采集频率,减少CPU占用
    }

    // 安全释放资源
    safe_release();
    std::cout << "[INFO] 程序正常退出" << std::endl;
    return 0;
}

4.2 编译+运行采集代码

# 编译(显式链接拜耳转换所需的OpenCV模块)
g++ v4l2_bayer_capture.cpp -o v4l2_bayer_capture \
-I/usr/local/include/opencv4 \
-L/usr/local/lib \
-lopencv_core -lopencv_imgproc -lopencv_highgui -lv4l2 -lpthread

# 运行(验证采集+拜耳转换)
./v4l2_bayer_capture

4.3 验证方法+兜底方案

  • 正常:弹出窗口显示彩色画面,无偏色、无卡顿,内存占用≤500MB(用top查看);
  • 画面偏色:修改拜耳转换标志(试以下选项):
cv::cvtColor(bayer_8bit, bgr, cv::COLOR_BayerRG2BGR);  // RG排列
cv::cvtColor(bayer_8bit, bgr, cv::COLOR_BayerBG2BGR);  // BG排列
cv::cvtColor(bayer_8bit, bgr, cv::COLOR_BayerGR2BGR);  // GR排列
  • 内存溢出:
    降低分辨率到 320x240(修改WIDTH=320, HEIGHT=240);
    关闭显示窗口(注释cv::imshow相关代码);
  • 编译报错:确认 OpenCV 4.8.0 已编译imgproc模块,重新编译 OpenCV 时确保BUILD_opencv_imgproc=ON。

阶段5:集成Yolov8n模型推理

核心目标:基于拜尔转换后的BGR帧,用OpenCV DNN运行Yolov8n推理,推理耗时<=150ms,FPS>=3s,内存占用<1.5GB。

5.1 准备YOLOv8n模型

# 下载YOLOv8n ONNX模型(6MB,极致轻量化)
wget https://github.com/ultralytics/assets/releases/download/v8.0.0/yolov8n.onnx -O yolov8n.onnx

# 验证模型完整性
ls -l yolov8n.onnx  # 大小约6MB

可以手动下载后传到树莓派:

scp yolov8n.onnx pi@你的树莓派IP:/home/pi/

核心问题本质
YOLOv8n 依赖的Resize算子在opset9中无适配版本,ultralytics 会强制升级到opset18(对应 IR 版本 10),导致无法导出真正的 opset9/IR9 模型,而你的 ONNX Runtime 1.17.0 仅支持 IR 版本 9,版本不兼容问题无法通过调整导出参数解决。
最终根因:ONNX Runtime 1.15.1 ARM64 版仍仅支持 IR9,YOLOv8n 导出的 IR10 模型需手动降级
此前所有尝试的核心卡点:YOLOv8n 导出时因 Resize 算子无法自动降级到 IR9,且 ONNX Runtime 1.15.1 ARM64 版仍只支持 IR9。解决方案:用 ONNX 工具手动修复 Resize 算子并将模型 IR 版本强制降级到 9(不是改 ONNX Runtime,而是改模型)。

# covert_ir_version.py
import onnx
from onnx import version_converter, helper

def convert_ir_version(model_path, output_path, target_ir=9):
    # 加载模型
    model = onnx.load(model_path)
    
    # 第一步:修复Resize算子(替换为opset9兼容版本)
    graph = model.graph
    new_nodes = []
    for node in graph.node:
        if node.op_type == "Resize" and "mode" in node.attribute:
            # 替换Resize算子的mode属性为opset9兼容格式
            new_node = helper.make_node(
                "Resize",
                inputs=node.input,
                outputs=node.output,
                name=node.name,
                coordinate_transformation_mode="asymmetric",
                cubic_coeff_a=-0.5,
                mode="nearest",
                nearest_mode="floor",
            )
            new_nodes.append(new_node)
        else:
            new_nodes.append(node)
    
    # 替换图中的节点
    del graph.node[:]
    graph.node.extend(new_nodes)
    
    # 第二步:强制降级IR版本到9
    model.ir_version = target_ir
    model.opset_import[0].version = 9  # opset版本同步到9
    
    # 保存修复后的模型
    onnx.save(model, output_path)
    print(f"✅ 模型已修复并降级到IR{target_ir},保存为:{output_path}")
    
    # 验证版本
    new_model = onnx.load(output_path)
    print(f"✅ 新模型IR版本:{new_model.ir_version},opset版本:{new_model.opset_import[0].version}")

# 执行转换(输入:原yolov8n.onnx,输出:yolov8n_ir9.onnx)
convert_ir_version("yolov8n.onnx", "yolov8n_ir9.onnx", target_ir=9)

5.2 完整推理代码

创建文件yolov8n_bayer_detect.cpp

#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
#include <chrono>
#include <vector>

// 核心配置:拜耳采集+YOLOv8n推理
#define WIDTH 640
#define HEIGHT 480
#define FPS 15
#define DEVICE "/dev/video0"
#define BAYER_FMT V4L2_PIX_FMT_SBGGR10
#define MODEL_PATH "yolov8n.onnx"
#define INF_SIZE 480  // 推理尺寸(480x480,比640x640省内存)
#define CONF_THRESH 0.4  // 高置信度阈值,减少后处理计算
#define IOU_THRESH 0.5

// YOLOv8n类别名(精简版,仅保留常用类别,省内存)
const std::vector<std::string> CLASSES = {
    "person", "bicycle", "car", "motorcycle", "bus", "truck",
    "cat", "dog", "bird", "chair", "tv", "laptop", "cell phone"
};

// V4L2缓冲区(仅1个)
struct Buffer {
    void* start;
    size_t length;
} buffer;

int fd;
cv::dnn::Net net;  // YOLOv8n模型

// 初始化V4L2(复用阶段4的逻辑)
bool init_v4l2() {
    fd = open(DEVICE, O_RDWR | O_NONBLOCK, 0);
    if (fd < 0) { perror("Open camera failed"); return false; }

    struct v4l2_format fmt = {0};
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = WIDTH;
    fmt.fmt.pix.height = HEIGHT;
    fmt.fmt.pix.pixelformat = BAYER_FMT;
    fmt.fmt.pix.field = V4L2_FIELD_NONE;
    if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) { perror("Set format failed"); return false; }

    struct v4l2_requestbuffers req = {0};
    req.count = 1;
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;
    if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) { perror("Request buffer failed"); return false; }

    struct v4l2_buffer buf = {0};
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    buf.index = 0;
    if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) { perror("Query buffer failed"); return false; }
    buffer.length = buf.length;
    buffer.start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
    if (buffer.start == MAP_FAILED) { perror("Mmap failed"); return false; }

    ioctl(fd, VIDIOC_QBUF, &buf);
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(fd, VIDIOC_STREAMON, &type);
    return true;
}

// 初始化YOLOv8n模型(轻量化配置)
bool init_yolov8n() {
    // 加载ONNX模型,仅用CPU,省内存
    net = cv::dnn::readNetFromONNX(MODEL_PATH);
    net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
    net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);
    // 禁用多线程(2GB内存避免冲突)
    net.setNumThreads(1);
    return !net.empty();
}

// 采集帧+拜耳转换(复用阶段4逻辑)
cv::Mat capture_frame() {
    struct v4l2_buffer buf = {0};
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    buf.index = 0;

    int ret;
    do {
        ret = ioctl(fd, VIDIOC_DQBUF, &buf);
        if (ret < 0 && errno == EAGAIN) { usleep(1000); }
    } while (ret < 0);

    // 拜耳转换
    cv::Mat bayer_10bit(HEIGHT, WIDTH, CV_16UC1, buffer.start);
    cv::Mat bayer_8bit;
    bayer_10bit.convertTo(bayer_8bit, CV_8UC1, 1.0/4.0);
    cv::Mat bgr;
    cv::cvtColor(bayer_8bit, bgr, cv::COLOR_BayerGB2BGR);

    ioctl(fd, VIDIOC_QBUF, &buf);
    return bgr;
}

// YOLOv8n推理+后处理(轻量化)
void infer_yolov8n(cv::Mat& frame) {
    // 预处理:640x480→480x480,归一化,转CHW
    cv::Mat blob = cv::dnn::blobFromImage(
        frame, 1.0/255.0, cv::Size(INF_SIZE, INF_SIZE),
        cv::Scalar(), true, false, CV_32F
    );
    net.setInput(blob);

    // 推理计时(监控耗时)
    auto start = std::chrono::high_resolution_clock::now();
    cv::Mat outputs = net.forward();
    auto end = std::chrono::high_resolution_clock::now();
    float infer_time = std::chrono::duration<float, std::milli>(end - start).count();

    // 后处理(精简逻辑,省内存)
    int rows = outputs.size[2];
    int cols = outputs.size[1];
    outputs = outputs.reshape(1, rows);

    std::vector<cv::Rect> boxes;
    std::vector<float> scores;
    std::vector<int> class_ids;

    for (int i = 0; i < rows; i++) {
        cv::Mat row = outputs.row(i).colRange(4, cols);
        float conf = *std::max_element(row.begin<float>(), row.end<float>());
        if (conf < CONF_THRESH) continue;

        int class_id = std::distance(row.begin<float>(), std::max_element(row.begin<float>(), row.end<float>()));
        float cx = outputs.at<float>(i, 0) * WIDTH / INF_SIZE;
        float cy = outputs.at<float>(i, 1) * HEIGHT / INF_SIZE;
        float w = outputs.at<float>(i, 2) * WIDTH / INF_SIZE;
        float h = outputs.at<float>(i, 3) * HEIGHT / INF_SIZE;

        int x1 = static_cast<int>(cx - w / 2);
        int y1 = static_cast<int>(cy - h / 2);
        int x2 = static_cast<int>(cx + w / 2);
        int y2 = static_cast<int>(cy + h / 2);

        boxes.push_back(cv::Rect(x1, y1, x2 - x1, y2 - y1));
        scores.push_back(conf);
        class_ids.push_back(class_id);
    }

    // 非极大值抑制(NMS,精简计算)
    std::vector<int> indices;
    cv::dnn::NMSBoxes(boxes, scores, CONF_THRESH, IOU_THRESH, indices);

    // 绘制检测框(精简绘制,省CPU)
    for (int i : indices) {
        cv::Rect box = boxes[i];
        cv::rectangle(frame, box, cv::Scalar(0, 255, 0), 2);
        std::string label = CLASSES[class_ids[i]] + " " + cv::format("%.2f", scores[i]);
        cv::putText(frame, label, cv::Point(box.x, box.y - 5),
                    cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 255, 0), 1);
    }

    // 显示推理耗时和FPS
    static int frame_count = 0;
    static float total_time = 0;
    frame_count++;
    total_time += infer_time;
    float fps = frame_count / (total_time / 1000);
    cv::putText(frame, cv::format("Infer: %.1fms | FPS: %.1f", infer_time, fps),
                cv::Point(10, 30), cv::FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar(255, 0, 0), 2);
}

// 释放资源
void release_resources() {
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(fd, VIDIOC_STREAMOFF, &type);
    munmap(buffer.start, buffer.length);
    close(fd);
}

int main() {
    // 初始化V4L2+YOLOv8n
    if (!init_v4l2()) { return -1; }
    if (!init_yolov8n()) { perror("Load YOLOv8n failed"); return -1; }

    // 推理主循环(精简显示,省内存)
    cv::namedWindow("YOLOv8n Detection (Bayer 640x480)", cv::WINDOW_NORMAL);
    cv::resizeWindow("YOLOv8n Detection (Bayer 640x480)", 640, 480);

    while (true) {
        cv::Mat frame = capture_frame();
        if (frame.empty()) continue;

        // 推理+绘制
        infer_yolov8n(frame);

        // 显示画面
        cv::imshow("YOLOv8n Detection (Bayer 640x480)", frame);
        if (cv::waitKey(1) == ord('q')) break;
    }

    // 释放资源
    release_resources();
    cv::destroyAllWindows();
    return 0;
}

5.3 编译+运行推理代码

# 编译(链接DNN模块,适配拜耳+推理)
g++ yolov8n_bayer_detect.cpp -o yolov8n_bayer_detect \
-I/usr/local/include/opencv4 \
-L/usr/local/lib \
-lopencv_core -lopencv_imgproc -lopencv_highgui -lopencv_dnn -lv4l2 -lpthread

# 运行(实时推理)
./yolov8n_bayer_detect

5.4 验证标准+兜底优化

  • 验证结果(2GB 内存达标)
    推理耗时≤150ms,平均 FPS≥3;
    内存占用≤1.5GB(用top查看,RES列≤1500MB);
    画面无偏色,检测框标注准确。
  • 兜底优化(若卡顿 / 内存溢出)
    降低推理尺寸到 416x416(修改INF_SIZE=416);
    进一步精简类别列表(只保留 1-2 个目标类别);
    关闭显示窗口(注释cv::imshow,仅保存结果):
// 替换显示代码为保存图片
static int save_count = 0;
if (save_count % 10 == 0) {  // 每10帧保存1张
    cv::imwrite(cv::format("detect_%d.jpg", save_count), frame);
}
save_count++;

增大swap到2GB(临时)

sudo dphys-swapfile swapoff
sudo nano /etc/dphys-swapfile  # 修改CONF_SWAPSIZE=2048
sudo dphys-swapfile setup && sudo dphys-swapfile swapon
  • 核心优化总结(适配 2GB 内存 + 拜耳摄像头)
    采集层:640x480 分辨率 + 1 个缓冲区 + 15fps,拜耳转换时即时释放临时内存;
    推理层:YOLOv8n 模型 + 480x480 推理尺寸 + 单线程推理,高置信度阈值减少计算;
    显示层:不放大窗口,精简绘制逻辑,降低显存 / CPU 占用;
    系统层:仅分配 128MB GPU 内存,剩余内存留给推理。
  • 最终验证清单
    阶段 3:v4l2-ctl采集拜耳格式无报错;
    阶段 4:V4L2 采集代码显示彩色画面,内存≤500MB;
    阶段 5:YOLOv8n 推理 FPS≥3,无 OOM 报错,检测框准确。
// yolov8n_v4l2_capture.cpp
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
#include <cstdio>
#include <cstdlib>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <pthread.h>
#include <atomic>
#include <chrono>

// OpenCV 4.8.0 头文件
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>

// 全局常量定义(改回YOLOv8n)
#define CAMERA_DEVICE "/dev/video0"  
#define YOLO_MODEL_PATH "yolov8n.onnx"  // 修正模型路径
#define INPUT_WIDTH 640                 // 改回640(兼容版模型默认)
#define INPUT_HEIGHT 640               
#define CONF_THRESHOLD 0.25            
#define NMS_THRESHOLD 0.45             
#define CLASS_NUM 80                   

// 全局变量
cv::dnn::Net yolo_net;                
int cam_fd = -1;                      
struct v4l2_buffer buf;               
struct v4l2_requestbuffers req;       
void* buffer_start[4] = {NULL};       
std::atomic<bool> is_running{true};   
std::vector<std::string> class_names; 

// 检测结果结构体
struct DetectionResult {
    int class_id;
    float confidence;
    cv::Rect bbox;
};

// 加载COCO类别名称
bool load_class_names(const std::string& path = "coco.names") {
    std::ifstream file(path);
    if (!file.is_open()) {
        std::cerr << "Warning: coco.names not found, use default class IDs" << std::endl;
        return false;
    }
    std::string line;
    while (std::getline(file, line)) {
        class_names.push_back(line);
    }
    file.close();
    return true;
}

// 初始化YOLOv8n ONNX模型(适配OpenCV 4.8.0 ARM)
bool init_yolov8n() {
    // 加载YOLOv8n模型(修正路径)
    yolo_net = cv::dnn::readNet(YOLO_MODEL_PATH);
    if (yolo_net.empty()) {
        std::cerr << "Error: Load YOLOv8n ONNX model failed!" << std::endl;
        return false;
    }

    // ARM CPU适配
    yolo_net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
    yolo_net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);

    std::cout << "YOLOv8n model loaded successfully!" << std::endl;
    return true;
}

// 初始化V4L2摄像头(通用YUYV格式)
bool init_v4l2_camera() {
    cam_fd = open(CAMERA_DEVICE, O_RDWR | O_NONBLOCK);
    if (cam_fd < 0) {
        std::cerr << "Error: Open camera " << CAMERA_DEVICE << " failed!" << std::endl;
        return false;
    }

    struct v4l2_capability cap;
    if (ioctl(cam_fd, VIDIOC_QUERYCAP, &cap) < 0) {
        std::cerr << "Error: Query camera capability failed!" << std::endl;
        close(cam_fd);
        return false;
    }

    if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
        std::cerr << "Error: Camera does not support video capture!" << std::endl;
        close(cam_fd);
        return false;
    }

    // 强制通用YUYV格式(避免兼容问题)
    struct v4l2_format fmt;
    memset(&fmt, 0, sizeof(fmt));
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = 640;
    fmt.fmt.pix.height = 480;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
    fmt.fmt.pix.field = V4L2_FIELD_NONE;

    if (ioctl(cam_fd, VIDIOC_S_FMT, &fmt) < 0) {
        std::cerr << "Error: Set video format failed!" << std::endl;
        close(cam_fd);
        return false;
    }

    memset(&req, 0, sizeof(req));
    req.count = 4;
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;

    if (ioctl(cam_fd, VIDIOC_REQBUFS, &req) < 0) {
        std::cerr << "Error: Request buffers failed!" << std::endl;
        close(cam_fd);
        return false;
    }

    for (int i = 0; i < req.count; i++) {
        memset(&buf, 0, sizeof(buf));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = i;

        if (ioctl(cam_fd, VIDIOC_QUERYBUF, &buf) < 0) {
            std::cerr << "Error: Query buffer " << i << " failed!" << std::endl;
            return false;
        }

        buffer_start[i] = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, cam_fd, buf.m.offset);
        if (buffer_start[i] == MAP_FAILED) {
            std::cerr << "Error: Mmap buffer " << i << " failed!" << std::endl;
            return false;
        }

        if (ioctl(cam_fd, VIDIOC_QBUF, &buf) < 0) {
            std::cerr << "Error: Enqueue buffer " << i << " failed!" << std::endl;
            return false;
        }
    }

    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (ioctl(cam_fd, VIDIOC_STREAMON, &type) < 0) {
        std::cerr << "Error: Start stream failed!" << std::endl;
        return false;
    }

    std::cout << "V4L2 camera initialized successfully!" << std::endl;
    return true;
}

// YOLOv8n推理函数(适配640×640兼容版模型)
std::vector<DetectionResult> yolov8n_infer(cv::Mat& frame) {
    std::vector<DetectionResult> results;

    // 预处理(YOLOv8标准预处理)
    cv::Mat blob;
    cv::dnn::blobFromImage(frame, blob, 1.0 / 255.0, cv::Size(INPUT_WIDTH, INPUT_HEIGHT), 
                           cv::Scalar(0, 0, 0), true, false);
    
    yolo_net.setInput(blob);
    cv::Mat output = yolo_net.forward();

    // YOLOv8输出格式:[1, 84, 8400](640×640分辨率对应8400个检测框)
    int num_detections = output.size[2];
    int num_params = output.size[1];

    for (int i = 0; i < num_detections; i++) {
        // 提取置信度最高的类别
        float max_conf = 0.0;
        int class_id = -1;
        for (int j = 4; j < num_params; j++) {
            float conf = output.at<float>(0, j, i);
            if (conf > max_conf) {
                max_conf = conf;
                class_id = j - 4;
            }
        }

        if (max_conf < CONF_THRESHOLD) continue;

        // 解析边界框(归一化xywh转像素坐标)
        float cx = output.at<float>(0, 0, i) * frame.cols;
        float cy = output.at<float>(0, 1, i) * frame.rows;
        float w = output.at<float>(0, 2, i) * frame.cols;
        float h = output.at<float>(0, 3, i) * frame.rows;

        int x1 = std::max(0, static_cast<int>(cx - w / 2));
        int y1 = std::max(0, static_cast<int>(cy - h / 2));
        int x2 = std::min(frame.cols-1, static_cast<int>(cx + w / 2));
        int y2 = std::min(frame.rows-1, static_cast<int>(cy + h / 2));

        results.push_back({class_id, max_conf, cv::Rect(x1, y1, x2 - x1, y2 - y1)});
    }

    // NMS非极大值抑制
    std::vector<int> indices;
    std::vector<cv::Rect> bboxes;
    std::vector<float> confidences;
    std::vector<int> class_ids;

    for (auto& res : results) {
        bboxes.push_back(res.bbox);
        confidences.push_back(res.confidence);
        class_ids.push_back(res.class_id);
    }

    cv::dnn::NMSBoxes(bboxes, confidences, CONF_THRESHOLD, NMS_THRESHOLD, indices);

    std::vector<DetectionResult> final_results;
    for (int idx : indices) {
        final_results.push_back({class_ids[idx], confidences[idx], bboxes[idx]});
    }

    return final_results;
}

// 绘制检测结果
void draw_results(cv::Mat& frame, const std::vector<DetectionResult>& results) {
    for (auto& res : results) {
        cv::rectangle(frame, res.bbox, cv::Scalar(0, 255, 0), 2);
        std::string label = class_names.empty() ? 
                           "Class " + std::to_string(res.class_id) : 
                           class_names[res.class_id];
        label += " " + std::to_string(static_cast<int>(res.confidence * 100)) + "%";
        cv::putText(frame, label, cv::Point(res.bbox.x, res.bbox.y - 10),
                    cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 255, 0), 2);
    }
}

// 释放V4L2资源
void release_v4l2() {
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(cam_fd, VIDIOC_STREAMOFF, &type);

    for (int i = 0; i < req.count; i++) {
        if (buffer_start[i] != MAP_FAILED) {
            munmap(buffer_start[i], buf.length);
        }
    }

    close(cam_fd);
    cam_fd = -1;
}

int main() {
    load_class_names();

    if (!init_yolov8n()) {
        return -1;
    }

    if (!init_v4l2_camera()) {
        return -1;
    }

    cv::Mat frame;
    while (is_running) {
        fd_set fds;
        FD_ZERO(&fds);
        FD_SET(cam_fd, &fds);

        struct timeval tv = {1, 0};
        int ret = select(cam_fd + 1, &fds, NULL, NULL, &tv);
        if (ret < 0) {
            std::cerr << "Error: Select failed!" << std::endl;
            break;
        } else if (ret == 0) {
            std::cerr << "Warning: Select timeout!" << std::endl;
            continue;
        }

        memset(&buf, 0, sizeof(buf));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;

        if (ioctl(cam_fd, VIDIOC_DQBUF, &buf) < 0) {
            std::cerr << "Error: Dequeue buffer failed!" << std::endl;
            continue;
        }

        // 转换YUYV到BGR
        cv::Mat raw_frame(480, 640, CV_8UC2, buffer_start[buf.index]);
        cv::cvtColor(raw_frame, frame, cv::COLOR_YUV2BGR_YUYV);

        if (ioctl(cam_fd, VIDIOC_QBUF, &buf) < 0) {
            std::cerr << "Error: Re-enqueue buffer failed!" << std::endl;
            break;
        }

        // YOLOv8推理
        auto start = std::chrono::high_resolution_clock::now();
        std::vector<DetectionResult> results = yolov8n_infer(frame);
        auto end = std::chrono::high_resolution_clock::now();
        float infer_time = std::chrono::duration<float, std::milli>(end - start).count();

        // 绘制结果
        draw_results(frame, results);
        cv::putText(frame, "Infer time: " + std::to_string(infer_time) + "ms", 
                    cv::Point(10, 30), cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255, 0, 0), 2);

        cv::imshow("YOLOv8n V4L2 Capture", frame);

        char key = cv::waitKey(1);
        if (key == 27) {
            is_running = false;
            break;
        }

        frame.release();
    }

    release_v4l2();
    cv::destroyAllWindows();

    std::cout << "Program exited normally!" << std::endl;
    return 0;
}
// yolov8n_ort_capture.cpp
#include <iostream>
#include <vector>
#include <string>
#include <cstdio>
#include <cstdlib>
#include <atomic>
#include <chrono>
#include <cstring>
#include <unistd.h>

// OpenCV头文件
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/dnn/dnn.hpp>
#include <opencv2/imgcodecs/imgcodecs.hpp>

// ONNX Runtime头文件
#include <onnxruntime_cxx_api.h>

// 全局常量
#define YOLO_MODEL_PATH "yolov8n.onnx"
#define INPUT_WIDTH 480
#define INPUT_HEIGHT 400
#define CONF_THRESHOLD 0.25
#define NMS_THRESHOLD 0.45
#define INPUT_FRAME_PATH "camera_frame.yuv" // 本地YUV帧文件
#define OUTPUT_FRAME_PATH "detection_result.jpg"

// 全局变量
Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "YOLOv8n"};
Ort::Session* yolo_session = nullptr;
Ort::RunOptions run_options;
std::string input_name;
std::string output_name;

// 检测结果结构体
struct DetectionResult {
    int class_id;
    float confidence;
    cv::Rect bbox;
};

// ONNX Runtime初始化(适配1.23.1 API)
bool init_yolov8n() {
    try {
        Ort::SessionOptions session_options;
        session_options.SetIntraOpNumThreads(4);
        session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_BASIC);

        // 检查模型文件
        if (access(YOLO_MODEL_PATH, F_OK) == -1) {
            std::cerr << "Error: Model file " << YOLO_MODEL_PATH << " not found!" << std::endl;
            return false;
        }

        // 加载模型
        yolo_session = new Ort::Session(env, YOLO_MODEL_PATH, session_options);

        // 获取输入输出名称
        size_t input_count = yolo_session->GetInputCount();
        size_t output_count = yolo_session->GetOutputCount();
        if (input_count == 0 || output_count == 0) {
            std::cerr << "Error: Model input/output is empty!" << std::endl;
            return false;
        }

        auto input_names = yolo_session->GetInputNames();
        auto output_names = yolo_session->GetOutputNames();
        input_name = input_names[0];
        output_name = output_names[0];

        std::cout << "✅ YOLOv8n model loaded successfully (ONNX Runtime 1.23.1)!" << std::endl;
        std::cout << "  - Input tensor name: " << input_name << std::endl;
        std::cout << "  - Output tensor name: " << output_name << std::endl;
        return true;
    } catch (const Ort::Exception& e) {
        std::cerr << "❌ Load YOLOv8n model failed! " << e.what() << std::endl;
        return false;
    }
}

// 读取YUV帧文件(640x480 YUYV格式)
bool read_yuv_frame(const std::string& path, cv::Mat& frame) {
    // 打开YUV文件
    FILE* fp = fopen(path.c_str(), "rb");
    if (!fp) {
        std::cerr << "❌ Failed to open YUV file: " << path << std::endl;
        return false;
    }

    // 读取YUYV数据(640x480x2字节)
    cv::Mat yuyv_frame(INPUT_HEIGHT, INPUT_WIDTH, CV_8UC2);
    size_t read_size = fread(yuyv_frame.data, 1, INPUT_WIDTH*INPUT_HEIGHT*2, fp);
    fclose(fp);

    if (read_size != INPUT_HEIGHT*INPUT_WIDTH*2) {
        std::cerr << "❌ YUV file size error! Expected " << INPUT_WIDTH*INPUT_HEIGHT*2 << " bytes, got " << read_size << std::endl;
        return false;
    }

    // 转换YUYV到BGR(OpenCV可处理的格式)
    cv::cvtColor(yuyv_frame, frame, cv::COLOR_YUV2BGR_YUYV);
    std::cout << "✅ Read YUV frame successfully! Resolution: " << frame.cols << "x" << frame.rows << std::endl;
    return true;
}

// YOLOv8n推理(核心检测功能)
std::vector<DetectionResult> yolov8n_infer(cv::Mat& frame) {
    std::vector<DetectionResult> results;

    // 预处理:尺寸转换+归一化+通道转换
    cv::Mat resized_frame;
    cv::resize(frame, resized_frame, cv::Size(INPUT_WIDTH, INPUT_HEIGHT));
    resized_frame.convertTo(resized_frame, CV_32F, 1.0 / 255.0);

    // 转换为NCHW格式(RGB)
    std::vector<cv::Mat> channels(3);
    cv::split(resized_frame, channels);
    std::vector<float> input_data(3 * INPUT_WIDTH * INPUT_HEIGHT, 0.0f);
    int idx = 0;
    for (int c = 0; c < 3; c++) {
        for (int h = 0; h < INPUT_HEIGHT; h++) {
            for (int w = 0; w < INPUT_WIDTH; w++) {
                input_data[idx++] = channels[c].at<float>(h, w);
            }
        }
    }

    // 设置输入张量
    auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeCPU);
    std::vector<int64_t> input_shape = {1, 3, INPUT_HEIGHT, INPUT_WIDTH};
    Ort::Value input_tensor = Ort::Value::CreateTensor<float>(
        memory_info, input_data.data(), input_data.size(), input_shape.data(), input_shape.size()
    );

    // 推理
    const char* input_names[] = {input_name.c_str()};
    const char* output_names[] = {output_name.c_str()};
    auto start_infer = std::chrono::high_resolution_clock::now();
    auto output_tensors = yolo_session->Run(run_options, input_names, &input_tensor, 1, output_names, 1);
    auto end_infer = std::chrono::high_resolution_clock::now();
    float infer_time = std::chrono::duration<float, std::milli>(end_infer - start_infer).count();
    std::cout << "✅ YOLOv8n inference done! Time: " << infer_time << "ms" << std::endl;

    // 解析输出
    float* output_data = output_tensors[0].GetTensorMutableData<float>();
    auto output_shape = output_tensors[0].GetTensorTypeAndShapeInfo().GetShape();
    
    if (output_shape.size() != 3 || output_shape[0] != 1 || output_shape[1] != 84 || output_shape[2] != 8400) {
        std::cerr << "⚠️ Invalid output shape! Expected [1,84,8400], got [";
        for (size_t i = 0; i < output_shape.size(); i++) {
            std::cerr << output_shape[i] << (i == output_shape.size()-1 ? "]" : ",");
        }
        std::cerr << std::endl;
        return results;
    }

    int num_params = static_cast<int>(output_shape[1]);
    int num_detections = static_cast<int>(output_shape[2]);

    // 解析检测结果
    for (int i = 0; i < num_detections; i++) {
        float max_conf = 0.0f;
        int class_id = -1;
        for (int j = 4; j < num_params; j++) {
            int data_idx = j * num_detections + i;
            float conf = output_data[data_idx];
            if (conf > max_conf && conf > CONF_THRESHOLD) {
                max_conf = conf;
                class_id = j - 4;
            }
        }
        if (class_id == -1 || max_conf < CONF_THRESHOLD) continue;

        // 解析坐标
        float x = output_data[0 * num_detections + i] * frame.cols;
        float y = output_data[1 * num_detections + i] * frame.rows;
        float w = output_data[2 * num_detections + i] * frame.cols;
        float h = output_data[3 * num_detections + i] * frame.rows;

        // 计算有效bbox
        int left = std::max(0, static_cast<int>(x - w / 2));
        int top = std::max(0, static_cast<int>(y - h / 2));
        int right = std::min(frame.cols - 1, static_cast<int>(x + w / 2));
        int bottom = std::min(frame.rows - 1, static_cast<int>(y + h / 2));

        results.push_back({class_id, max_conf, cv::Rect(left, top, right - left, bottom - top)});
    }

    // NMS非极大值抑制
    std::vector<int> indices;
    std::vector<cv::Rect> bboxes;
    std::vector<float> confidences;
    std::vector<int> class_ids;
    for (auto& res : results) {
        bboxes.push_back(res.bbox);
        confidences.push_back(res.confidence);
        class_ids.push_back(res.class_id);
    }
    cv::dnn::NMSBoxes(bboxes, confidences, CONF_THRESHOLD, NMS_THRESHOLD, indices);

    // 筛选最终结果
    std::vector<DetectionResult> final_results;
    for (int idx : indices) {
        final_results.push_back({class_ids[idx], confidences[idx], bboxes[idx]});
    }

    return final_results;
}

// 绘制检测结果
void draw_results(cv::Mat& frame, const std::vector<DetectionResult>& results) {
    if (results.empty()) {
        std::cout << "ℹ️ No objects detected in the frame!" << std::endl;
        return;
    }

    std::cout << "✅ Detected " << results.size() << " objects:" << std::endl;
    for (auto& res : results) {
        // 绘制检测框
        cv::rectangle(frame, res.bbox, cv::Scalar(0, 255, 0), 2);
        // 绘制标签
        std::string label = "Class " + std::to_string(res.class_id) + " (" + std::to_string(res.confidence).substr(0, 4) + ")";
        cv::putText(frame, label, cv::Point(res.bbox.x, res.bbox.y - 5), 
                    cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 255, 0), 1);
        
        // 打印结果
        std::cout << "  - Class " << res.class_id << ", Confidence: " << res.confidence 
                  << ", BBox: (" << res.bbox.x << "," << res.bbox.y << "," 
                  << res.bbox.width << "," << res.bbox.height << ")" << std::endl;
    }
}

int main() {
    // 1. 初始化YOLOv8n模型
    if (!init_yolov8n()) {
        return -1;
    }

    // 2. 读取本地YUV帧文件
    cv::Mat frame;
    if (!read_yuv_frame(INPUT_FRAME_PATH, frame)) {
        delete yolo_session;
        return -1;
    }

    // 3. 执行YOLOv8n检测
    auto results = yolov8n_infer(frame);

    // 4. 绘制结果并保存
    draw_results(frame, results);
    if (cv::imwrite(OUTPUT_FRAME_PATH, frame)) {
        std::cout << "✅ Detection result saved to: " << OUTPUT_FRAME_PATH << std::endl;
    } else {
        std::cerr << "❌ Failed to save detection result!" << std::endl;
    }

    // 5. 释放资源
    delete yolo_session;

    std::cout << "\n🎉 Program exited normally! Core YOLOv8n detection function works." << std::endl;
    return 0;
}
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐