尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linux下V4L2+Qt USB摄像头采集显示完整指南

Linux下V4L2+Qt USB摄像头采集显示完整指南 简介这是一份基于V4L2与Qt5.6.0的USB摄像头采集显示程序源码面向Linux下从事嵌入式或桌面视频应用的开发者解决从摄像头读取视频流并在GUI中实时显示的实际问题。资源共56个文件以C语言源文件30个c、头文件16个h为主搭配少量C源文件3个cpp、Qt工程文件、UI界面文件以及README、ChangeLog等说明文档压缩包仅164KB结构紧凑便于阅读源码。目前已有1879人学习。代码完整覆盖V4L2设备初始化、格式设置、mmap图像捕获再到QImage转换与界面刷新的全流程并封装为qcamera_v4l2_lib库展示了底层驱动接口与高层UI框架之间的集成方式对想同时掌握Linux视频采集和Qt图形编程的开发者有直接参考价值。1. 从零搭建采集链路环境准备与设备检查先说点实际的。前几天帮一个做工业视觉的朋友调程序他在Linux下用Qt写USB摄像头采集代码逻辑看着没问题Qt界面也起得来就是采集窗口一直是黑屏。排查到最后发现是V4L2的设备节点选错了——他用的USB摄像头实际注册在/dev/video2程序里硬编码写的/dev/video0而/dev/video0被笔记本内置摄像头占用了。这种问题在实机调试中太常见了所以这篇文章我把自己完整跑通的一套方案整理出来从设备检查到显示刷新每一步都讲清楚原理和踩过的坑。本文的目标是给出一份可以直接参考的Linux下V4L2 Qt的USB摄像头采集显示实现方案。无论你是做嵌入式Linux开发、智能车视觉还是桌面端的图像采集工具这套链路基本都能覆盖。我尽量把底层原理讲透把容易翻车的地方标出来建议配合代码一起看。1.1 确认摄像头设备节点在写任何代码之前第一件事永远是确认设备节点。Linux下UVC协议USB Video Class的摄像头设备几乎都注册为/dev/video*但不一定从video0开始排。我自己的经验是先用以下命令把所有video节点列出来看看哪个是对应的设备ls -l /dev/video* v4l2-ctl --list-devices第一行v4l2-ctl --list-devices是v4l-utils工具包提供的它能把USB设备名和video节点对应关系直接打出来比ls /dev清晰得多。如果没有这个命令先安装sudo apt-get install v4l-utils实测输出大概是这样的USB Camera: USB Camera (usb-0000:00:14.0-1): /dev/video0 /dev/video1注意一个摄像头往往会出现两个video节点——video0是采集节点video1是metadata节点用于传输3A统计信息等元数据。选错节点会导致VIDIOC_QUERYCAP返回ENOTTY错误程序打开设备失败。判断方法很简单用v4l2-ctl --all -d /dev/video0看Capabilities是否带Video Capture标志。1.2 验证相机基本工作状态设备节点确认后先不要急着写代码用工具直接验证一下采集是否正常。这一步可以排除摄像头硬件本身的问题免得后面在代码里排查半天最后发现是摄像头坏了。sudo apt-get install ffmpeg ffplay -f v4l2 -input_format mjpeg -video_size 640x480 /dev/video0或者用v4l2-ctl直接抓一帧v4l2-ctl --device/dev/video0 --set-fmt-videowidth640,height480,pixelformatYUYV --stream-mmap --stream-count1 --stream-toframe.yuyv如果能看到画面说明硬件链路没问题问题一定出在软件侧。注意ffplay如果不加-input_format mjpeg可能默认走YUYV格式对带宽要求高1080p下容易花屏或卡顿——这也是后面程序里要处理格式协商的原因。1.3 Qt开发环境准备采集显示程序涉及Qt的QImage、QPixmap和定时器刷新所以需要安装带GUI支持的Qt开发环境。我用的是Qt 5.15.2 qmake如果你用Qt 6代码逻辑基本不变但注意QImage构造和像素格式的枚举值没有变可以直接迁移。sudo apt-get install qtbase5-dev qtbase5-dev-tools libqt5core5a libqt5gui5 libqt5widgets5工程文件里需要加QT core gui widgets如果你还想做图像处理相关的扩展后面加opencv模块也不冲突。编译时建议加-O2优化后续做图像操作时性能差距明显。2. V4L2采集的核心链路open、格式协商、mmap、队列这一节是整个程序的心脏。V4L2本身是一套标准搞懂了这套ioctl的调用序列不管接USB摄像头还是CSI接口的树莓派摄像头逻辑都通用。整个采集流程可以分成三个阶段打开设备、配置格式、流式采集。2.1 打开设备与能力查询用open()系统调用打开设备节点注意打开方式是O_RDWR而且不要加O_NONBLOCK——虽然V4L2支持非阻塞模式但配合select()用起来很绕新手容易把时序搞乱。我自己习惯用阻塞模式加多线程的方式后面会讲。打开后第一个ioctl是VIDIOC_QUERYCAP查询设备能力确认驱动类型是V4L2_CAP_VIDEO_CAPTURE。这一步能过滤掉部分非采集设备比如某些USB转HDMI采集卡可能注册为V4L2_CAP_VIDEO_OVERLAY走不同的通路。#include fcntl.h #include sys/ioctl.h #include linux/videodev2.h int fd open(/dev/video0, O_RDWR); if (fd 0) { perror(open); return -1; } struct v4l2_capability cap; memset(cap, 0, sizeof(cap)); if (ioctl(fd, VIDIOC_QUERYCAP, cap) 0) { perror(VIDIOC_QUERYCAP); close(fd); return -1; } if (!(cap.capabilities V4L2_CAP_VIDEO_CAPTURE)) { fprintf(stderr, 设备不支持视频采集\n); close(fd); return -1; }driver字段会打印驱动名常见的USB摄像头是uvcvideo树莓派CSI摄像头是bm2835 mmal或unicam。看到驱动名对排查问题很有帮助。2.2 格式协商与分辨率设置摄像头支持哪些格式、哪些分辨率不是猜测出来的要主动去查询。这里有个很关键的点USB摄像头默认返回的格式列表里通常会同时包含YUYV和MJPEG两种MJPEG是压缩格式传输压力小但需要解码YUYV是裸流格式带宽消耗大但可以直接转RGB显示。我推荐程序里优先尝试MJPEG采集尤其在做高分辨率1080p、4K采集的时候。USB 2.0的理论带宽是480Mbps实际有效吞吐量大概在280Mbps左右1080p30fps的YUYV裸流需要640Mbps远超带宽上限根本跑不起来。但MJPEG格式下1080p30fps的典型码流只有30-80Mbps完全在带宽范围内。代价是每帧需要软解JPEG会占一定CPU但现代处理器基本可以忽略。设置格式的核心ioctl是VIDIOC_S_FMTstruct 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_MJPEG; // 或 V4L2_PIX_FMT_YUYV fmt.fmt.pix.field V4L2_FIELD_NONE; if (ioctl(fd, VIDIOC_S_FMT, fmt) 0) { perror(VIDIOC_S_FMT); return -1; }设置完之后fmt.fmt.pix.width和height会回填成摄像头实际支持的分辨率。有的摄像头你设置640x480它会自动调整到最接近的支持分辨率比如800x600所以一定要以回填值为准后面申请缓冲区时要用这个值计算大小。另外强烈建议调用VIDIOC_S_PARM调整帧率struct v4l2_streamparm parm; memset(parm, 0, sizeof(parm)); parm.type V4L2_BUF_TYPE_VIDEO_CAPTURE; parm.parm.capture.timeperframe.numerator 1; parm.parm.capture.timeperframe.denominator 30; // 30fps ioctl(fd, VIDIOC_S_PARM, parm);某些摄像头不设置帧率默认走保守的15fps甚至5fps低光场景自动降帧如果不主动请求30fps显示效果卡顿但你又不知道问题出在哪。2.3 mmap缓冲区申请与队列管理V4L2的采集方式有三种read/write直接读写、mmap内存映射、userptr用户指针。实际项目里mmap最常用也是稳定性和性能最平衡的方案。它的思路是驱动在内核空间分配缓冲区用户空间通过mmap映射到进程地址空间采集时驱动把数据写入缓冲区用户程序直接访问对应内存地址不需要一次次的拷贝。申请缓冲区的流程如下struct v4l2_requestbuffers req; memset(req, 0, sizeof(req)); req.count 4; // 申请4个缓冲区 req.type V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory V4L2_MEMORY_MMAP; if (ioctl(fd, VIDIOC_REQBUFS, req) 0) { perror(VIDIOC_REQBUFS); return -1; }缓冲区数量我一般申请4个。太少比如2个可能导致驱动的DMA来不及轮转出现丢帧太多比如8个虽然更平滑但会增加内存占用和帧延迟。4是实践中的平衡点对USB摄像头尤其合适——USB传输本身的burst特性决定了帧到达不平滑小的缓冲队列容易溢出。然后逐个查询缓冲区的内存大小和偏移量做映射struct v4l2_buffer buf; void* buffers[4]; size_t buf_lengths[4]; for (int i 0; i 4; i) { memset(buf, 0, sizeof(buf)); buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory V4L2_MEMORY_MMAP; buf.index i; ioctl(fd, VIDIOC_QUERYBUF, buf); buf_lengths[i] buf.length; buffers[i] mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); if (buffers[i] MAP_FAILED) { perror(mmap); return -1; } }映射完成后要把缓冲区全部加入驱动采集队列告诉驱动这些缓冲区可以拿来存帧了for (int i 0; i 4; i) { memset(buf, 0, sizeof(buf)); buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory V4L2_MEMORY_MMAP; buf.index i; ioctl(fd, VIDIOC_QBUF, buf); }最后启动采集enum v4l2_buf_type type V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl(fd, VIDIOC_STREAMON, type);至此驱动开始持续往缓冲区写帧数据。2.4 循环采集DQBUF与QBUF的配合采集循环的核心就是两个ioctlVIDIOC_DQBUF从队列里取出一个已经填好数据的缓冲区VIDIOC_QBUF把它重新放回队列。这一取一放构成了流水线while (running) { struct v4l2_buffer buf; memset(buf, 0, sizeof(buf)); buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory V4L2_MEMORY_MMAP; // 取出一个装满数据的缓冲区 ioctl(fd, VIDIOC_DQBUF, buf); // 处理图像数据这里拿到的是 buffers[buf.index] 指向的内存 process_image(buffers[buf.index], buf.bytesused); // 处理完把缓冲区放回队列供驱动继续使用 ioctl(fd, VIDIOC_QBUF, buf); }buf.bytesused是实际数据大小。MJPEG格式下这个值跟分辨率不是线性关系JPEG压缩比动态变化所以不能用width * height * 2去推算数据量一定要用bytesused字段。YUYV格式下则固定是width * height * 2。一个常见的低级错误是进程结束后忘记VIDIOC_STREAMOFF导致摄像头LED灯一直亮着、设备被占用。程序退出时必须做三件事STREAMOFF停止采集、munmap解除映射、close关闭设备。3. 数据落地的两个关键问题格式转换与内存管理从V4L2拿到的原始数据不能直接丢给Qt显示。因为Qt的QImage原生支持RGB32ARGB32、RGB565等格式但不认识YUYV更不认识MJPEG。这一步不做显示出来的画面要么是花的要么是绿屏。3.1 YUYV转RGB的数学原理YUYV是YUV 4:2:2打包格式每个像素都采样亮度Y色度U/V每两个像素共享一组。存储顺序是Y0 U0 Y1 V0——即两个像素占4个字节。转换公式是标准BT.601R 1.164 * (Y - 16) 1.596 * (V - 128) G 1.164 * (Y - 16) - 0.813 * (V - 128) - 0.391 * (U - 128) B 1.164 * (Y - 16) 2.018 * (U - 128)实际代码里为了性能我会用整数运算和查表法。Y、U、V的范围都是0-255可以预先建三个256项的查找表转换时直接查表避免浮点计算。一次转换查6次表Y通道共用在640x480分辨率下速度提升非常明显。// 预计算查表 int table_r[256][256], table_g[256][256], table_b[256][256]; for (int y 0; y 256; y) { for (int u 0; u 256; u) { int y16 1.164 * (y - 16); int u128 u - 128; table_b[y][u] clamp(y16 2.018 * u128, 0, 255); } } // 同理构建 table_g 和 table_r完整转换函数的耗时在640x480下大约需要5-8ms取决于CPU性能如果只是显示用途这个开销可接受。但如果你做了更重的处理比如目标检测建议把格式转换丢给OpenCV的cvtColor它的SIMD优化能把这个时间压到1ms以内。3.2 MJPEG帧的解码MJPEG本质就是一帧一帧的JPEG图片。解码有两种常见方案用Qt的QImageReader把内存字节比如QByteArray传给QImageReader调用read()生成QImage。优点是代码极少缺点是每帧都要创建读取器对象有一定开销但实测在720p下依然能跑30fps。用libjpeg-turbo性能最优设置jpeg_mem_src和jpeg_read_scanlines就能逐行解码CPU占用比方案一低20%左右。如果你在嵌入式平台上做采集这个方案更合适。我自己的习惯是桌面端调试用Qt方案够稳上板子或做低延迟应用换libjpeg-turbo。代码上预留接口避免后期重写。// 用 QImageReader 解码 MJPEG 帧 QByteArray jpegData((const char*)buffers[buf.index], buf.bytesused); QBuffer buffer(jpegData); buffer.open(QIODevice::ReadOnly); QImageReader reader(buffer, jpeg); QImage frame reader.read();read()返回的QImage格式默认是ARGB32可以直接丢给后面显示。3.3 mmap缓冲区复用与生命周期内存这块有个常被忽略的点VIDIOC_DQBUF拿到缓冲区后数据只在QBUF放回之前有效。如果你把帧数据存到QImage里而QImage的构造方式只是浅拷贝不复制像素数据那么下一次QBUF之后这片内存可能被驱动覆写导致显示花屏。正确的做法有两种深拷贝QImage copy frame.copy();像素数据会复制到新分配的内存。提前转换在QBUF之前完成转换把转换后的RGB数据存入自己管理的QImage。我很早踩过这个坑——画面周期性出现条纹和块状噪声排查了很久才发现是浅拷贝导致缓冲区被覆写。所以这里特别提醒在QBUF之前帧数据要么处理完要么复制走没有第三条路。4. Qt显示刷新从QImage到界面渲染的优化有了帧数据剩下的问题是怎么把它高效地刷到界面上。这里同样有几个选择不同选择影响性能和代码复杂度。4.1 直接把QImage转换为QPixmap在Qt中QPixmap是专门为屏幕显示优化的绘制效率比直接画QImage高尤其在X11/Wayland下QPixmap可能走GPU加速。常规写法void Widget::updateFrame(const QImage frame) { QPixmap pixmap QPixmap::fromImage(frame); // 注意缩放保持宽高比 QPixmap scaled pixmap.scaled(ui-label-size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); ui-label-setPixmap(scaled); }这里有个小陷阱scaled()每帧都执行如果窗口大小固定比如640x480不动缩放其实是不必要的可以直接setPixmap省掉一次图像缩放的开销。而且scaled()的质量参数用Qt::FastTransformation还是SmoothTransformation在实时显示场景下差异不大但前者快得多。4.2 避免重复构造QImage采集循环里频繁构造QImage对象会带来大量堆分配。更高效的做法是提前创建好目标QImage每次用memcpy把转换后的RGB数据直接拷贝到QImage的像素缓冲区。这个做法在Qt里用QImage::bits()拿到像素指针然后按行拷贝// 初始化时创建目标图像 QImage displayImage(width, height, QImage::Format_RGB888); size_t frameSize width * height * 3; // 每帧完成后把数据拷入QImage的缓冲区 memcpy(displayImage.bits(), rgbBuffer, frameSize);这样每次更新省去了QImage构造和析构的开销在上位机跑长时间采集时能显著减少内存碎片。4.3 用定时器驱动刷新而不是线程直接操作UIV4L2采集线程抓到帧之后不能直接在采集线程里操作UI组件QLabel::setPixmap因为Qt的UI不是线程安全的。常见的做法是用信号槽把帧数据发回主线程。但每帧都发一个信号信号队列会积压导致UI响应变慢。我更推荐的做法是采集线程只负责把帧数据写入一个共享的最新帧缓冲区用std::atomic标志位标记有新帧可用。主线程跑一个QTimer比如每33ms触发一次每次检查标志位为真时取走帧并刷新界面。这样采集线程和UI线程解耦即使采集帧率波动UI刷新频率也稳定。// mainwindow.h public slots: void onTimer(); private: std::shared_ptrQImage latestFrame; std::atomicbool frameReady{false}; // mainwindow.cpp MainWindow::MainWindow() { QTimer *timer new QTimer(this); connect(timer, QTimer::timeout, this, MainWindow::onTimer); timer-start(33); } void MainWindow::onTimer() { if (frameReady.exchange(false)) { // 此时latestFrame一定有效 QPixmap pixmap QPixmap::fromImage(*latestFrame); ui-label-setPixmap(pixmap); } }采集线程那侧抓到帧后做深拷贝更新latestFrame然后置位frameReady。不用mutex因为赋值操作在x86/ARM上对指针是原子的加上std::atomic的同步语义就够了。这个设计跑一天一夜也不会崩也不会有UI卡顿。5. 避坑记录五类高频问题的完整排查链路这部分是我在实际调试中踩过的坑汇总每个问题都附上排查链路和最终解法。5.1VIDIOC_REQBUFS返回ENOTTY或EBUSY这个错误最常见的隐藏原因设备节点被其他进程占用。很多人会忽略——摄像头LED亮着但程序还是打不开设备因为浏览器、QQ、会议软件等后台占用了摄像头。排查方法sudo lsof /dev/video0如果显示有进程占用关掉对应的软件再测试。如果确认没有其他进程占用则需要检查驱动是否加载dmesg | grep uvcvideo sudo modprobe uvcvideo5.2 颜色偏绿或花屏一图胜千言——花屏通常不是代码逻辑错了而是格式不匹配。我有一次调了个把小时最后发现是摄像头实际输出NV12格式但程序按YUYV解析。措辞上的小差异造成的视觉影响却天差地别。排查链路先跑v4l2-ctl --list-formats -d /dev/video0确认硬件到底支持哪些格式。再看程序里VIDIOC_S_FMT之后fmt.fmt.pix.pixelformat回填成了什么。对照回填值编写对应的解析逻辑。NV12转RGB和YUYV转RGB的公式完全不同不能混用。5.3 Qt程序启动报no platform plugin could be initialized这个问题在Linux下也很常见。通常是Qt的platform插件路径找不到因为Qt的platforms目录没有正确加载。排查链路检查QT_QPA_PLATFORM_PLUGIN_PATH环境变量是否指向了正确的platforms目录。确认程序运行目录和部署结构是否正确——如果直接从构建目录跑应该能自动找到插件但如果你把可执行文件拷到其他目录运行就需要手动设置环境变量或把lib/plugins/platforms放到预期位置。export QT_QPA_PLATFORM_PLUGIN_PATH/path/to/qt/plugins/platforms ./your_app如果还不想配置环境变量可以在代码里调用QApplication::addLibraryPath()提前把插件路径加进去实测有效。5.4 画面卡顿但CPU占用不高CPU占用不高说明不是解码或转换瓶颈问题多半出在采集帧率。很多USB摄像头默认帧率是5-15fps你程序里设置30fps可能被忽略了。取证方法v4l2-ctl --list-frameintervals -d /dev/video0 --width640 --height480 --pixelformatYUYV输出会列出该分辨率支持的帧率范围比如30.00 fps或25.00 fps。如果硬件最高只支持25fps程序再努力也到不了30。另外USB摄像头插在USB 2.0和USB 3.0接口上支持的帧率也不同有条件优先插USB 3.0口。5.5 程序退出后摄像头LED不灭或设备仍被占用这个问题几乎100%是忘了做清理操作。正确处理顺序// 停流 enum v4l2_buf_type type V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl(fd, VIDIOC_STREAMOFF, type); // 解除mmap映射 for (int i 0; i 4; i) { munmap(buffers[i], buf_lengths[i]); } // 关闭设备 close(fd);如果你用了Qt的信号槽或线程还要确保采集线程真正退出后再执行上述清理否则存在竞态条件。这里有经验的做法用一个std::atomicbool控制采集循环退出用thread.join()等待线程结束后再清理资源。别小看这一步很多程序退出后摄像头还被占用的Bug查来查去最后都是线程没退干净。6. 进阶扩展同一套架构如何适配MJPEG高分辨率与图像处理项目如果继续往下走有几个很自然的扩展方向。我把自己的探索和经验也一并分享出来方便你在这个基础上做二次开发。6.1 从YUYV切到MJPEG分辨率翻倍的工程改动前面说过USB 2.0的带宽瓶颈决定了高分辨率必须走MJPEG。切到MJPEG后改动点其实不多主要是解码环节替换。我实测在1080p30fps下用libjpeg-turbo解码MJPEGCPU占用约10-15%i5-8250U而YUYV格式下带宽根本不支持直接卡成幻灯片。另一个隐藏收益是MJPEG格式下同一分辨率的数据量更小memcpy和后续处理的缓存命中率更高整体延迟反而更低。做嵌入式采集时这个优势更明显——ARM平台的DDR带宽本就有限压缩格式能省出带宽给算法用。6.2 叠加OSD信息与帧率统计采集程序里显示实时帧率几乎是必备功能。在Qt里可以在paintEvent里用QPainter往图像上叠加文字QPainter painter(displayImage); painter.setPen(Qt::green); painter.setFont(QFont(Monospace, 12)); painter.drawText(10, 20, QString(FPS: %1).arg(currentFps));这个操作在每帧刷新时执行开销很小。帧率统计用滑动窗口平均法更重要——不要只算最近一秒的均值会跳跃而是用最近30帧的平均间隔显示更平滑。6.3 和OpenCV结合做实时处理如果你的目标不止显示还要做人脸检测、颜色追踪之类的处理建议把图像流转给OpenCV。V4L2采集到的帧数据在转换成RGB后直接构造cv::Matcv::Mat frame(height, width, CV_8UC3, rgbBuffer);这里有个关键点cv::Mat默认是BGR通道顺序但QImage::Format_RGB888是RGB顺序。统一用cv::cvtColor转一次COLOR_RGB2BGR后续所有处理都发生在OpenCV的BGR空间最后显示前再转回RGB给Qt。如果不统一顺序颜色检测类算法会出各种怪异结果。6.4 树莓派/嵌入式平台的移植要点这套架构在树莓派上同样适用但有几个差异点要留意树莓派CSI接口的摄像头比如OV5647用的不是UVC驱动而是bm2835-v4l2驱动但它对上层仍暴露标准的V4L2接口所以代码不用改动。ARM平台的QImage::scaled()性能不如x86尽量在初始化时固定缩放尺寸避免每帧动态计算。树莓派上如果使用GPU加速记得在qputenv(QT_QUICK_BACKEND, software)和QT_OPENGL相关参数上做实验——实测在树莓派4B上软件渲染反而比脆弱的GPU加速稳定。7. 一个可以快速跑通的完整最小工程理论讲完给一个最小可运行的示例工程。我把上面所有的关键点揉在一起代码结构清晰你可以直接拿来做模板改。7.1 工程文件proQT core gui widgets TARGET v4l2_qt_camera TEMPLATE app SOURCES main.cpp \ mainwindow.cpp \ v4l2camera.cpp HEADERS mainwindow.h \ v4l2camera.h7.2 V4L2采集线程类v4l2camera.h/cpp// v4l2camera.h #ifndef V4L2CAMERA_H #define V4L2CAMERA_H #include QThread #include QImage #include QString #include atomic #include memory class V4L2Camera : public QThread { Q_OBJECT public: V4L2Camera(QString devPath, int width, int height, QObject *parent nullptr); ~V4L2Camera(); void stop(); bool openDevice(); std::shared_ptrQImage getLatestFrame(); bool hasNewFrame(); void clearNewFrameFlag(); protected: void run() override; private: QString m_devPath; int m_width; int m_height; int m_fd; std::atomicbool m_running; std::atomicbool m_frameReady; std::shared_ptrQImage m_frame; }; #endif // V4L2CAMERA_H// v4l2camera.cpp #include v4l2camera.h #include fcntl.h #include sys/ioctl.h #include sys/mman.h #include linux/videodev2.h #include cstring #include cstdio // 详见上文各 ioctl 调用此处不重复贴全部代码实际工程中粘贴上面代码即可这里不全部贴出v4l2camera.cpp的完整代码——上面各个小节里已经给出了核心函数的实现逻辑。工程层面你需要做的就是把上下文串联起来openDevice()做设备打开和格式协商run()跑DQBUF/QBUF循环循环内解码并更新m_framestop()置位m_running并STREAMOFF。7.3 主界面类mainwindow.h/cpp// mainwindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H #include QMainWindow #include QLabel #include QTimer #include memory #include v4l2camera.h class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent nullptr); private slots: void onTimer(); private: V4L2Camera *camera; QLabel *displayLabel; QTimer *refreshTimer; }; #endif // MAINWINDOW_H// mainwindow.cpp #include mainwindow.h #include QVBoxLayout MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { displayLabel new QLabel(this); displayLabel-setMinimumSize(640, 480); setCentralWidget(displayLabel); camera new V4L2Camera(/dev/video0, 640, 480, this); if (!camera-openDevice()) { displayLabel-setText(打开摄像头失败); } else { camera-start(); } refreshTimer new QTimer(this); connect(refreshTimer, QTimer::timeout, this, MainWindow::onTimer); refreshTimer-start(33); } void MainWindow::onTimer() { if (camera-hasNewFrame()) { auto frame camera-getLatestFrame(); camera-clearNewFrameFlag(); displayLabel-setPixmap(QPixmap::fromImage(*frame)); } }7.4 编译运行qmake make ./v4l2_qt_camera运行后应该能直接看到摄像头画面。如果黑屏回到第5节的问题排查链路按图索骥。最后分享一个实操技巧调试阶段给程序加-v命令行参数打印每次VIDIOC_S_FMT回填的格式和分辨率、每帧的bytesused变化趋势。这些日志信息量不大但对定位问题作用巨大——很多画面不对的问题看输出数值就能一眼识破。我在实际项目中一直保留这个调试选项建议你也加上。本文还有配套的精品资源点击获取
返回列表