(ubuntu+codeblock+wxwidgets+v4l2)實現攝像頭讀取視頻並拍照

 

 

寫在前面

  研一上就開始斷斷續續地幫老師進行一些項目工作,一個學期基本是在接觸身份認證的工作。使用的平臺從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,如果您發現了,不嫌煩,請務必告知我,不勝感激。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章