树莓派YOLOv8n部署
文章目录
嵌入式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;
}
更多推荐
所有评论(0)