当前位置: 首页 > news >正文

保姆级教程:在Ubuntu 22.04上用V4L2从摄像头抓取一张JPEG图片(附完整代码)

从零开始:Ubuntu 22.04下V4L2摄像头JPEG抓图实战指南

当我们需要在Linux系统中快速验证摄像头功能或进行简单的图像采集时,V4L2(Video for Linux 2)框架无疑是最直接的选择。本文将带你从设备识别到完整JPEG图片保存,一步步实现这个看似简单却暗藏玄机的过程。

1. 环境准备与设备识别

在开始编码前,我们需要确保系统环境已就绪。Ubuntu 22.04默认已包含V4L2驱动支持,但建议先更新系统:

sudo apt update && sudo apt upgrade -y

连接摄像头后,首先需要确认系统是否正确识别设备。执行以下命令查看设备节点:

ls /dev/video*

常见问题排查

  • 如果看不到任何video设备,尝试重新插拔摄像头
  • 在虚拟机环境中,确保已正确配置USB设备直通
  • 对于VirtualBox/VMware用户,建议将USB控制器设置为3.1版本(后面会解释原因)

识别到设备后,可以使用v4l2-ctl工具快速检查设备能力:

v4l2-ctl --list-devices v4l2-ctl --list-formats --device=/dev/video0

提示:不同摄像头支持的格式可能不同,常见的有YUYV、MJPG等。MJPG格式通常能提供更好的压缩率。

2. V4L2编程基础框架

V4L2编程遵循一套标准流程,主要包括以下步骤:

  1. 打开设备文件
  2. 查询设备能力
  3. 设置视频格式
  4. 申请缓冲区
  5. 开始视频流
  6. 捕获帧数据
  7. 停止视频流
  8. 释放资源

下面是一个基础代码框架:

#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <linux/videodev2.h> int main() { int fd = open("/dev/video0", O_RDWR); if (fd < 0) { perror("无法打开设备"); return -1; } // 后续操作将在这里添加 close(fd); return 0; }

3. 设备能力与格式设置

了解设备支持的功能和格式是成功采集的关键。我们需要使用VIDIOC_QUERYCAPVIDIOC_ENUM_FMT这两个ioctl命令。

完整格式查询示例

struct v4l2_capability cap = {0}; if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) { perror("查询设备能力失败"); return -1; } printf("驱动名称: %s\n", cap.driver); printf("设备名称: %s\n", cap.card); if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { fprintf(stderr, "设备不支持视频采集\n"); return -1; } struct v4l2_fmtdesc fmt = {0}; fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; printf("支持的格式:\n"); for (fmt.index = 0; ioctl(fd, VIDIOC_ENUM_FMT, &fmt) >= 0; fmt.index++) { printf(" %d: %s (%.4s)\n", fmt.index, fmt.description, (char*)&fmt.pixelformat); }

设置视频格式时,我们需要考虑分辨率和像素格式。以下代码设置640x480的MJPG格式:

struct v4l2_format format = {0}; format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; format.fmt.pix.width = 640; format.fmt.pix.height = 480; format.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; format.fmt.pix.field = V4L2_FIELD_ANY; if (ioctl(fd, VIDIOC_S_FMT, &format) < 0) { perror("设置格式失败"); return -1; } printf("实际设置: %dx%d, 格式: %.4s\n", format.fmt.pix.width, format.fmt.pix.height, (char*)&format.fmt.pix.pixelformat);

4. 缓冲区管理与内存映射

V4L2提供了两种数据采集方式:read()直接读取和内存映射。对于性能要求较高的场景,内存映射是更好的选择。

缓冲区申请与映射流程

  1. 申请缓冲区
  2. 查询缓冲区信息
  3. 内存映射
  4. 将缓冲区加入队列
struct v4l2_requestbuffers req = {0}; req.count = 4; // 申请4个缓冲区 req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) { perror("申请缓冲区失败"); return -1; } struct buffer { void *start; size_t length; } *buffers = calloc(req.count, sizeof(*buffers)); for (unsigned int i = 0; i < req.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) { perror("查询缓冲区信息失败"); return -1; } buffers[i].length = buf.length; buffers[i].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); if (buffers[i].start == MAP_FAILED) { perror("内存映射失败"); return -1; } // 将缓冲区加入队列 if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) { perror("加入队列失败"); return -1; } }

5. 捕获帧数据与保存JPEG

一切准备就绪后,就可以开始捕获数据了。以下是完整的捕获流程:

// 开始视频流 enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (ioctl(fd, VIDIOC_STREAMON, &type) < 0) { perror("开启视频流失败"); return -1; } // 从队列中取出一个已填充的缓冲区 struct v4l2_buffer buf = {0}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) { perror("获取帧数据失败"); return -1; } // 将数据保存为JPEG文件 FILE *fp = fopen("capture.jpg", "wb"); if (!fp) { perror("无法创建文件"); return -1; } fwrite(buffers[buf.index].start, buf.bytesused, 1, fp); fclose(fp); // 将缓冲区重新加入队列 if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) { perror("重新加入队列失败"); return -1; } // 停止视频流 if (ioctl(fd, VIDIOC_STREAMOFF, &type) < 0) { perror("停止视频流失败"); return -1; }

6. 虚拟机环境特殊问题解决

在虚拟机环境中使用USB摄像头时,经常会遇到以下问题:

  1. 图片损坏或无法识别:表现为JPEG文件头错误
  2. 程序阻塞在VIDIOC_DQBUF:无法获取帧数据

这些问题通常是由于USB兼容性设置不当导致的。解决方案:

VirtualBox设置步骤

  1. 关闭虚拟机
  2. 进入设置 > USB
  3. 启用USB控制器
  4. 选择USB 3.1(xHCI)控制器
  5. 添加摄像头USB设备过滤器

VMware设置步骤

  1. 关闭虚拟机
  2. 进入虚拟机设置 > USB控制器
  3. 将兼容性设置为USB 3.1
  4. 确保已连接摄像头设备

注意:更改USB设置后,可能需要重新插拔摄像头或重启虚拟机才能生效。

7. 完整示例代码

以下是完整的JPEG抓图程序,整合了所有关键步骤:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <linux/videodev2.h> struct buffer { void *start; size_t length; }; int main() { int fd = open("/dev/video0", O_RDWR); if (fd < 0) { perror("无法打开设备"); return -1; } // 设置格式 struct v4l2_format format = {0}; format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; format.fmt.pix.width = 640; format.fmt.pix.height = 480; format.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; format.fmt.pix.field = V4L2_FIELD_ANY; if (ioctl(fd, VIDIOC_S_FMT, &format) < 0) { perror("设置格式失败"); close(fd); return -1; } // 申请缓冲区 struct v4l2_requestbuffers req = {0}; req.count = 4; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) { perror("申请缓冲区失败"); close(fd); return -1; } struct buffer *buffers = calloc(req.count, sizeof(*buffers)); if (!buffers) { perror("内存分配失败"); close(fd); return -1; } for (unsigned int i = 0; i < req.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) { perror("查询缓冲区信息失败"); goto error; } buffers[i].length = buf.length; buffers[i].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); if (buffers[i].start == MAP_FAILED) { perror("内存映射失败"); goto error; } if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) { perror("加入队列失败"); goto error; } } // 开始视频流 enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (ioctl(fd, VIDIOC_STREAMON, &type) < 0) { perror("开启视频流失败"); goto error; } // 捕获一帧 struct v4l2_buffer buf = {0}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) { perror("获取帧数据失败"); goto error; } // 保存JPEG文件 FILE *fp = fopen("capture.jpg", "wb"); if (!fp) { perror("无法创建文件"); goto error; } fwrite(buffers[buf.index].start, buf.bytesused, 1, fp); fclose(fp); // 重新加入队列 if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) { perror("重新加入队列失败"); goto error; } // 停止视频流 if (ioctl(fd, VIDIOC_STREAMOFF, &type) < 0) { perror("停止视频流失败"); goto error; } // 清理资源 for (unsigned int i = 0; i < req.count; i++) { munmap(buffers[i].start, buffers[i].length); } free(buffers); close(fd); return 0; error: for (unsigned int i = 0; i < req.count; i++) { if (buffers[i].start != MAP_FAILED) { munmap(buffers[i].start, buffers[i].length); } } free(buffers); close(fd); return -1; }

编译并运行:

gcc -o capture capture.c ./capture

运行成功后,当前目录下会生成capture.jpg文件,可以使用任意图片查看器打开。

8. 常见问题与调试技巧

在实际开发中,你可能会遇到以下问题:

1. 图片无法打开或损坏

  • 检查是否设置了正确的像素格式(V4L2_PIX_FMT_MJPEG)
  • 确认保存文件时使用了正确的数据长度(buf.bytesused)
  • 在虚拟机中尝试调整USB兼容性设置

2. 程序阻塞在VIDIOC_DQBUF

  • 确保已正确调用VIDIOC_STREAMON
  • 检查所有缓冲区是否已加入队列(VIDIOC_QBUF)
  • 在虚拟机中,尝试更换USB端口或重启服务

3. 分辨率不支持

  • 使用v4l2-ctl查看支持的分辨率:
    v4l2-ctl --list-formats-ext --device=/dev/video0
  • 在代码中选择设备支持的分辨率

4. 权限问题

  • 确保当前用户对/dev/video*设备有读写权限
  • 可以将用户加入video组:
    sudo usermod -aG video $USER

调试时可以增加详细的日志输出,帮助定位问题。例如,在每个关键步骤后打印状态信息:

printf("已成功设置格式: %dx%d, 格式: %.4s\n", format.fmt.pix.width, format.fmt.pix.height, (char*)&format.fmt.pix.pixelformat);

对于更复杂的调试,可以使用strace工具跟踪系统调用:

strace -o trace.log ./capture

掌握这些调试技巧后,即使遇到问题也能快速定位和解决。在实际项目中,我发现大多数问题都源于格式设置不正确或虚拟机环境配置不当,特别是USB兼容性设置。

http://www.cnnetsun.cn/news/2686142.html

相关文章:

  • 神经网络似然估计加速引力波数据分析
  • 手把手教你用示波器抓取Type-C充电‘握手’信号(附波形分析)
  • BI与AI融合:从数据报表到智能决策的实践路径
  • 告别报错!Win10下Autodock Vina 1.2.3完整安装与避坑指南(附批量脚本)
  • Cortex-M3调试状态检测原理与实现方法
  • 从零到一:用Godot 4.2制作你的第一个2D横版动作游戏(完整项目流程与避坑指南)
  • 别再死记硬背达西定律了!用Python模拟地下水流动,直观理解渗流速度与达西速度的区别
  • 3步极速突破:百度网盘解析工具完全指南
  • 手把手教你:VCSA安装后必做的三件事(改IP、开SSH、查磁盘)
  • 时间序列预测:从白噪声到积分模型的黄金基准实践
  • 手把手教你用TiDE预测电力负荷:从ETTh1数据集到自定义数据集的完整迁移教程
  • 普冉PY32F003呼吸灯调光太生硬?试试这个千分之一精度PWM平滑渐变方案
  • 在Ubuntu 20.04上搞定华为Atlas ATC环境:一份给AI开发者的保姆级避坑指南
  • 告别‘玄学’报错:手把手教你降级setuptools和wheel,成功安装Gym 0.18.3
  • PHP会话管理从入门到精通
  • 用游戏开发实战理解图形学:从关键帧动画到物理模拟,Unity/WebGL案例拆解
  • 用Java手撸一个Tomasulo算法模拟器:从看懂实验到理解动态调度的核心
  • 手把手教你用逻辑分析仪调试W25Q32 SPI Flash:从波形看懂擦、写、读全过程
  • Jetson Orin Nano 刷机踩坑记:从IMX477摄像头画面撕裂到JetPack 5.1.2升级成功
  • 别再只会拔插了!用xhci寄存器搞定USB3.0的三种复位(PowerOn/Warm/Hot Reset)
  • 全民AI时代:非技术背景者的个人实验入门指南与避坑清单
  • MACO框架:LLM驱动的CGRA软硬件协同设计
  • 别再一条条画线了!Visio 2021 高效连线与模具导入保姆级教程(附避坑指南)
  • 5分钟搞定!Blender 3MF插件让你的3D打印工作流效率翻倍 [特殊字符]
  • 告别‘pip不是命令’:Windows/Mac双平台环境变量配置全攻略(含Python 3.12+新特性避坑)
  • 从STM32到普冉PY32F003:UART通信代码移植与HAL库对比实战
  • VMware虚拟机共享文件夹设置详解:从Windows宿主机到Linux虚拟机的文件互传避坑指南
  • 银河麒麟服务器iSCSI配置避坑指南:从multipath多路径到开机自动挂载的完整流程
  • MaxEnt模型报错别慌!手把手教你用SDMToolbox搞定栅格数据范围对齐(附ArcGIS参数设置)
  • 别再手动打emoji了!用Rime小狼毫的联想滤镜,一键输入微信/飞书专属表情