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