写在开头

笔者自从学习了Framebuffer编程和V4L2编程之后,就想实现在LCD屏上显示实时视频

笔者学习过正点I.MX6U Linux C编程中的相关内容,然而原子的例程是针对OV5640摄像头写的,像素格式是RGB

然而USB摄像头大多支持MJPEG或者YUYV格式,如果要在屏幕上显示需要进行格式转换,而转换像素格式是需要处理时间的,自然会影响视频帧率

笔者尝试写过YUYV2RGB888或者YUYV2RGB565,实际跑起来能明显感受到掉帧;
同时,笔者也考虑通过OpenCV来对JPEG或者YUYV进行编解码来显示,然而实际效果也不尽如人意

那有没有办法既能实时显示视频也不用进行图像格式转换还能有用户操作界面?

笔者结合之前所学内容最终决定用QT写界面,V4L2编程来获取实时的帧并将其显示在设计好的界面上

开发环境

  1. 虚拟机Ubuntu 16.04
  2. 编辑器VsCode
  3. 交叉编译工具 arm-linux-gnueabihf
  4. 已制作文件系统,已使能UVC相关驱动
  5. 正点原子ZYNQ7010启明星开发板
  6. USB摄像头淘宝随便买的一个
  7. QT Creator 5.9.6

必备知识

UVC驱动配置可以看我的这一篇: Linux 内核4.14添加UVC配置

QT移植并显示图片可以看我的这一篇: Qt移植正点原子ZYNQ7010-Arm平台显示图片demo,本博客的程序设计也是基于这一篇文章的

V4L2编程入门可以看我的这一篇: V4L2编程之USB摄像头采集jpeg图像

界面布局设计

在这里插入图片描述
可以先看一下运行效果,主体部分一共就三个:一个QLabel控件用于显示实时图像,两个QPushButton,一个用于控制视频流的显示,另一个用于解放资源并关闭窗口。

界面很简单,本博客只是实现最基础的功能,后续会在此基础上加控件和其他功能。

程序设计思路

  1. 界面布局初始化
  2. 获得用户输入,打开对应摄像头
  3. 摄像头初始化,包括打印摄像头支持的像素格式、分辨率、帧率
  4. 设置采集格式,包括像素格式、帧率设置
  5. 申请内存空间并建立内存映射,进行出队入队操作
  6. 开启视频流
  7. 设置定时器,等待“开始”按钮信号触发定时器开启
  8. 定时器定时获取一帧图像将其显示在QLabel上
  9. 等待“结束”按钮信号触发释放资源并关闭窗口

关键部分在于789三步,这里稍作解释,不想看的可直接去看源码——我的Github仓库: Linux C编程实战代码
我已经将源码连同V4L2编程的API用户手册一并上传了,欢迎大家学习交流

要在ARM平台跑也是可以的,需要自行移植QT到ARM开发板,移植办法详见我往期博客Qt移植正点原子ZYNQ7010-Arm平台显示图片demo

关键代码分析

获取用户输入赋值给v4l2_dev_init()初始化对应的摄像头
引入QCoreApplication获取用户输入的第二个字段,例如执行 ./Qt_V4l2 /dev/video1,device_parm[1] 的内容即/dev/video1
值得注意的是需要进行一个数据类型转换才能作为open()函数的参数

    QStringList device_parm = QCoreApplication::arguments();
    QString str = device_parm[1];

    v4l2_dev_init(str.toStdString());
    
    ......
    
	int MainWindow::v4l2_dev_init(string device_name){
	    /* 打开摄像头 */
	    v4l2_fd = open(device_name.c_str(),O_RDWR);
	    if(v4l2_fd < 0){
	        printf("open camera failed\n");
	        return -1;
	    }
	    printf("open camera success\n");
	    
	    ......
	    
	 }

两个按钮的信号连接代码

    //点击开始按钮,打开定时器
    connect(pushButton[0],SIGNAL(clicked()),this,SLOT(timer_start()));

    //每隔固定的时间显示一帧
    timer = new QTimer();
    connect(timer, SIGNAL(timeout()), this, SLOT(video_show()));

    /* 按钮窗口关闭,先释放设备,再关闭窗口 */
    connect(pushButton[1],SIGNAL(clicked()),this,SLOT(v4l2_device_release()));
    connect(pushButton[1],SIGNAL(clicked()),this,SLOT(close()));

定时器控制帧率,每个33ms触发video_show()显示一帧数据,一秒钟正好显示30帧(笔者的摄像头最大支持30fps,故如此设置)

/* 定时器控制帧 */
void MainWindow::timer_start(){
    // 1000/33约等于30,也就是每一秒显示30帧
    timer->start(33);
}

初始化select()来进行I/O端口复用

    fd_set fds;
    FD_ZERO(&fds);
    FD_SET(v4l2_fd,&fds);

    //设置等待时间为2s
    struct timeval tv;
    tv.tv_sec = 2;
    tv.tv_usec = 0;
    select(v4l2_fd+1,&fds,NULL,NULL,&tv);

此段代码摘自V4L2 API手册最后的示例代码中,相关pdf文档已上传到github

获取一帧图像的数据,见于v4l2_get_one_frame(FrameBuffer *framebuf)
首先从出队队列中取一帧视频数据的视频缓冲区,再将该帧数据拷贝到frame中

    if(0 > ioctl(v4l2_fd,VIDIOC_DQBUF,&one_buf)){
        printf("VIDIOC_DQBUF failed!\n");
        return -1;
    }
	// bytesused 表示buf中已经使用的字节数
	    memcpy(framebuf->buf,(char *)buffer_infos[one_buf.index].start,one_buf.bytesused);
	    framebuf->length = one_buf.bytesused;

Qt 提供了四个用于处理图像数据的类,而 QPixmap 正是为在屏幕上显示图像而设计和优化
最后将获取保存在frame中的数据用QPixmap转换就可以显示了

     FrameBuffer frame;
     QPixmap pix;

     //获取一帧显示
     v4l2_get_one_frame(&frame);
     pix.loadFromData(frame.buf, frame.length);
     pix.scaled(displayLabel->width(),displayLabel->height(),Qt::KeepAspectRatio);
     displayLabel->setPixmap(pix);

结束语

本博客的程序已上传到github中,往期博客有关Linux C编程的代码也一并上传,需要的自行下载
地址:https://github.com/Huge-Hammer/Linux-C-Coding

后续会继续更新,通过程序设计来实现拍照截图、图像处理、视频流的存储和推流等功能,慢慢完善了

我是爱学习的诸葛铁锤,觉得有帮助的话记得点个赞再走吧,wakuwaku!

请添加图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐