(ubuntu+codeblock+wxwidgets+v4l2)实现摄像头读取视频并拍照

6 篇文章 0 订阅
2 篇文章 0 订阅

 

 

写在前面

  研一上就开始断断续续地帮老师进行一些项目工作,一个学期基本是在接触身份认证的工作。使用的平台从Windows,andriod最后集中攻克linux,实现国产系统的身份认证。期间经历了很多困难,好在我不排斥一直坐在电脑桌前研究新的实物,甚至有点乐在其中。只要该休息的时候能够休息,我不觉得有什么不开心的。学期末决定将linux的身份认证c/s架构实现(b/s架构已经实现)。这篇博客就是你先实现身份认证的摄像头采集照片部分。

  至于为什么基于wxwidgets,我只能安慰自己因为它是跨平台的库。那QT呢?不好意思我没用过,不过我相信,如果我用过,我一定不会再用wxwidgets(我的体验很糟糕)。至于摄像头的调用为什么使用v4l2,因为这是linux自带的视频处理接口,虽然接口调用繁杂,至少不用花时间装其它库(opencv)。好吧就说这么多开始吧。

零、整体流程图

一、v4l2调用流程

V4l2的调用流程非常的长,但是调理还算清晰:

(1)打开摄像头设备

在linux下面一般设备路径为/dev/videox

fd= open("/dev/video0", O_RDWR | O_NONBLOCK);

这里的O_NONBLOCK就是表面的意思,非阻塞模式。如果使用阻塞模式,则去掉这个选项

注意:非阻塞模式会导致后续的取数据操作不管成功与否都返回,这也会导致取不到数据导致判断错误,若没有取不到数据的相应处理,请采取阻塞模式。

(2)取得设备使用权

          struct v4l2_capability capability;
          int ret = ioctl(fd, VIDIOC_QUERYCAP, &capability);

后续讲只给出所需结构体和操作宏,皆使用ioctl函数操作设备

(3)选择视频输入设 

   struct v4l2_input input;//结构体用前先memset0
    VIDIOC_QUERYCAP

我只使用了一个摄像头,不多探究。

(4)检测支持的视频格式

    struct v4l2_fmtdesc fmtdesc;
    fmtdesc.index = 0;
    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    VIDIOC_ENUM_FMT

(5)设置捕捉的视频格式

    struct v4l2_format fmt;
    memset(&fmt,00,sizeof(fmt));
    fmt.type                = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width       = WITTH;//捕获的视频的大小
    fmt.fmt.pix.height      = HEIGHT;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;    //YUV4:2:2格式
    fmt.fmt.pix.field       = V4L2_FIELD_INTERLACED
   VIDIOC_S_FMT
  (6)向设备驱动申请缓存空间

    struct v4l2_requestbuffers req;
    req.count = CAP_BUF_NUM;    //never more than 5,i use 4
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;

 VIDIOC_REQBUFS

(7)获取缓存区信息,并进行映射

VIDIOC_QUERYBUF

typedef struct VideoBuffer {   //自定义结构体
                          void *start;
                          size_t length;
                  } VideoBuffer;
bool CamDialog::Dommap(int fd)
{    int n_buffers; //缓冲区索引
    buffers = (VideoBuffer*)calloc(CAP_BUF_NUM, sizeof(VideoBuffer));//dsitribute         mistake
    if(!buffers)
    {
        wxMessageBox(wxT("out of memory\n"),wxT("error"));
        return false;
    }
    for(n_buffers = 0;n_buffers < CAP_BUF_NUM;++n_buffers)
    {
        v4l2_buffer buf;
        memset(&buf,0,sizeof(buf));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = n_buffers;
        if(-1 == ioctl(fd,VIDIOC_QUERYBUF,&buf))    //请求缓冲区信息
        {
            wxMessageBox(wxT("VIDIOC_QUERYBUF: "),wxT("error"));
            return false;
        }
        buffers[n_buffers].length = buf.length;//映射缓冲区长度,应该等于w×h×2
        buffers[n_buffers].start = mmap(NULL, buf.length,//映射缓冲区起点
                                  PROT_READ | PROT_WRITE,
                                  MAP_SHARED,
                                  fd, buf.m.offset);
        if (buffers[n_buffers].start == MAP_FAILED)
        {
            wxMessageBox(wxT("MAP_FAILED "),wxT("error"));
            return false;
        }
    }
    return true;
}

(7)采集预备

VIDIOC_QBUF

VIDIOC_STREAMON

v4l2_buffer buf;
for(int i = 0;i<CAP_BUF_NUM;i++)
    {
        memset(&buf,0,sizeof(buf));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = i;
       // buf.m.offset = dev->buffer[i].offset;
       if(-1 == ioctl(fd,VIDIOC_QBUF, &buf))         //先将所有缓冲都加入队列,4个
       {
          wxMessageBox(wxT("VIDIOC_QBUF "),wxT("error"));
          return false;
       }
    }
    v4l2_buf_type type;
    type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    /*打开视频流*/
    if(-1 == ioctl(fd,VIDIOC_STREAMON, &type))
    {
        wxMessageBox(wxT("VIDIOC_STREAMON "),wxT("error"));
        return false;
    }

(8)从队列取视频帧

VIDIOC_DQBUF

     memset(&capture_buf, 0, sizeof(capture_buf));
     capture_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
     capture_buf.memory = V4L2_MEMORY_MMAP;
     int t =ioctl(fd, VIDIOC_DQBUF, &capture_buf);
     ioctl(fd, VIDIOC_DQBUF, &capture_buf);//注意此处若为非阻塞
/*(此处为取出数据首指针,数据为YUYV格式,注意若为非阻塞,则取出的数据可能是空*/
 this->pVideoData = (unsigned char *)buffers[capture_buf.index].start;

(9)将缓存放回队列

        if(-1 == ioctl(fd,VIDIOC_QBUF,&capture_buf))   

(10)停止视频采集,解除映射

     int ret = ioctl(fd, VIDIOC_STREAMOFF, &buf_type);

 munmap(buffer[j].start, buffer[j].length);

关于v4l2的更具体使用,建议参考以下链接

https://www.cnblogs.com/silence-hust/p/4464291.html

或者我在最后给的源码。

二、关于wxwidgets的使用

关于该库在codeblock的调用方式如果是建立空白文档引用库,则需要一些配置,配置方法也许我在其他文章会给出。另一种就是直接用codeblock新建wxwidgets空项目,然后使用wxformbiulder工具创造界面,自动生成源文件,再将源文件加入工程即可。wxformbuilder的ubuntu安装方法我后续也尽量补上。这里只说用到的wxwidgets的类与方法。

(1)入口函数

首先wxwidgets的程序必须要创建一个wxApp的子类,并重载OnInit虚函数,这个函数就相当于c的main函数,是程序的入口,从这个函数开始代码没有问题。

(2)一些GUI控件

我用到的控件类不多,

wxButton按钮

wxStaticBox静态窗口

不同的wx类都可能要调用相应的头文件,如果不知道可以在下面网站搜索在类的介绍开始出一般有头文件

https://docs.wxwidgets.org/3.1/index.html

这个网站英文好的话也可以当做变成参考,不要放过看不懂的信息,一般都很重要。

(3)事件触发机制

两种方法:

1.exit_button->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( CamDialog::OnQuit ), NULL, this );这是一种讲控件,触发事件的行为,事件行为绑定起来的方式

2.创建时间表,这个时间表应该放在cpp文件中

wxBEGIN_EVENT_TABLE(CamDialog,wxDialog)
    EVT_THREAD(MY_THREAD_EVENT_ID, CamDialog::MyThreadEvent)
END_EVENT_TABLE()

在头文件中声明:

#define MY_THREAD_EVENT_ID 100

DECLARE_EVENT_TABLE()

(4)线程调用:

这里采取一种简单的方式

首先先创建一个wxthread的子类。

class Mythread : public wxThread          //使用线程需要重载wxThread类
{
public:
    Mythread():wxThread(wxTHREAD_DETACHED){};
    virtual void *Entry();
private:};
   //start thread
    thread = new Mythread();
    //OpenCam();
    if(thread->Run()!=wxTHREAD_NO_ERROR)
    {
        wxLogError(wxT("cannot create thread!"));
        delete thread;
        thread = NULL;
    }

Entry就是子线程产生后要执行的函数,重写它即可,我们要用这个子线程来实现视频流循环读取,从而释放主线程去做别的事情。

(5)进程间通信

这里交代一下为什么需要进程间通信。这个问题困扰我很久,子线程成功启动了,也成功地读取了视频流并进行解析,但是总会在显示图片的步骤上程序崩溃。我找了找了很久原因,最终在之前给出的网站上找到了原因所在

由于一些底层的原因,不建议在子线程中操纵关于GUI的函数,这可能导致程序提前退出。我查了一下,这个现象在其他GUI框架中也存在。

也就是说虽然我可以在子线程处理视频流,却不能独立更改GUI,要告知主线程去做这件事。下面是告知方式:

1.定义事件号和声明事件表

#define MY_THREAD_EVENT_ID 100
DECLARE_EVENT_TABLE()

2.定义事件表

wxBEGIN_EVENT_TABLE(CamDialog,wxDialog)
    EVT_THREAD(MY_THREAD_EVENT_ID, CamDialog::MyThreadEvent)
END_EVENT_TABLE()

3.在子线程中向主线程发出通讯请求

 wxThreadEvent guiprocess(wxEVT_COMMAND_THREAD,MY_THREAD_EVENT_ID);
 wxQueueEvent(this,guiprocess.Clone());

(6)视频和图片显示

方法应该非常多,我用的以下方法,和mfc类似,但QT应该没有这么麻烦
 

wxMemoryDC memDC;//创建内存设备上下文 

wxImage image(WITTH,HEIGHT,rgb,true);//读取rgb数据存储为

image.Rescale(WITTH/2,HEIGHT/2);//缩放

wxBitmap bitmap(image);//创建位图

memDC.SelectObject(bitmap);//绑定内存设备上下文和位图

wxBufferedPaintDC dc(box);//创建屏幕dc,参数是控件对象,放在那里
dc.Blit(0,0,bitmap.GetWidth(),bitmap.GetHeight(),&memDC,0,0,wxCOPY,false);//复制

注意bitmap也有这样构造

wxBitmap bitmap((char*)rgb,640,480,1);

但仔细查看文档可知这个函数只能读取生成单色位图,第三个参数不是1都是不能正常运行的(这个也恶心我很久)

(7)wxwidgets的messagebox和messagelog的使用

wxMessageBox的使用非常简单,下面是一个最简单的例子

wxMessageBox(wxT("VIDIOC_QBUF: "),wxT("error"));

顺便讲一下wx字符串的构造方法,满足基本的使用

wxString msg;//首先构造一个字符串
msg.Printf("%s"xxxx);//通过该类方法构造字符串

wxMessageLog是一种输出日志的方法:

wxLogMessage(log);

值得一提的是,可以对日志的输出方式进行设置

 wxLog::SetActiveTarget(new wxLogStderr(Log_file)); //将日志输出到一个文件

wxMessageLog主要用来输出一些非错误信息的数据。

三、关于图像格式的转换

在第一部分的检测设备支持的格式一步中,可以查看设备输出的图像格式,一般为YUYV格式。

要将一帧显示在界面上,需要讲YUYV转换成rgb格式。网上的教程有很多,推荐下面的链接:

https://www.cnblogs.com/lknlfy/archive/2012/04/09/2439508.html

而若需要得到bmp格式的图片输出则需要将rgb数据转换为bmp格式,网上同样很多,推荐下面的链接:

https://www.cnblogs.com/betterwgo/p/6909787.html

图片保存出来可能是倒着的,那请修改类成员变量        bih.biHeight = -height;

图片的显色显示也可能是错的,这是rgb大小端存储的原因

在数据转换前,进行重新排序处理

for(i = 0; i < (w*h); i++)
{
	temp = *(readBuff + i*3);
	*(readBuff + i*3) = *(readBuff + i*3 + 2);
	*(readBuff + i*3 + 2) = temp;	
}

四、写在最后

整个流程下来,写得比较粗糙,读者可能看不懂,反正我写这些大部分是为了自己,如果有读者喜欢,都是写额外的慰藉。

另外可以参照我给的代码去看。demo可能有很多bug,如果您发现了,不嫌烦,请务必告知我,不胜感激。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值