Qt/Linux 下的攝像頭捕獲(Video4Linux2)

 

Linux下使用各種設備是一件令人興奮的事情。在Unix的世界裏,用戶與硬件打交待總是簡單的。最近筆者在Linux下搞了攝像頭的開發,有一點感想發於此處。

Linux中操作一個設備一般都是打開(open),讀取(read)和關閉(close)。使用Read的大多是一些字符型設備,然而對於顯示屏或者攝像頭這種字符設備而已,挨個字的讀寫將使得系統調用變得頻繁,衆所周之,系統調用對於系統而已是個不小的開銷。於是有內存映射(mmap)等物,本例中將講述在Linux下開發攝像頭的一般過程以及使用Qt進行界面開發的實例。

使用mmap方式獲取攝像頭數據的方式過程一般爲:

打開設備 -> 獲取設備的信息 -> 請求設備的緩衝區 -> 獲得緩衝區的開始地址及大小 -> 使用mmap獲得進程地址空間的緩衝區起始地址 -> 讀取緩衝區。

 

Mmap就是所謂內存映射。很多設備帶有自己的數據緩衝區,或者驅動本身在內核空間中維護一片內存區域,爲了讓用戶空間程序安全地訪問,內核往往要從設備內存或者內核空間內存複製數據到用戶空間。這樣一來便多了複製內存這個環節,浪費了時間。因此mmap就將目標存儲區域映射到一個用戶空間的一片內存,這樣用戶進程訪問這片內存時,內核將自動轉換爲訪問這個目標存儲區。這種轉換往往是地址的線性變化而已(很多設備的存儲空間在所謂外圍總線地址空間(X86)或者總的地址空間(ARM)上都是連續的),所以不必擔心其轉換的效率。

現在開始敘述Video4Linux2的使用。

/* 打開設備並進行錯誤檢查 */

int fd = open ("/dev/video",O_RDONLY);

if (fd==-1){

perror ("Can't open device");

return -1;

}

 

/* 查詢設備的輸出格式 */

struct v4l2_format format;

memset (&format,0,sizoef(format));

format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

if (-1==ioctl(fd,VIDIOC_G_FMT,&format)){

perror ("While getting format");

return -2;

}

 

/* 

 * 這裏要將struct v4l2_format結構體置零,然後將

 * format.type設定爲V4L2_BUF_TYPE_VIDEO_CAPTURE,

 * 這樣在進行 VIDIOC_G_FMT 的ioctl時,驅動就會知

 * 道是在捕獲視頻的情形下獲取格式的內容。

 * 成功返回後,format就含有捕獲視頻的尺寸大小及格

 * 式。格式存儲在 format.fmt.pix.pixelformat這個32

 * 位的無符號整數中,共四個字節,以小頭序存儲。這裏

 * 介紹一種獲取的方法。

 */

 

char code[5];

unsigned int i;

for (i=0;i<4;i++) {

code[i] = (format.fmt.pix.pixelformat & (0xff<<i*8))>>i*8;

}

code[4]=0;

 

/* 現在的code是一個以/0結束的字符串。很多攝像頭都是以格式MJPG輸出視頻的。

 * MJPG是Motion JPEG的縮寫,其實就是一些沒填霍夫曼表的JPEG圖片。

 */

 

/* 請求一定數量的緩衝區。

 * 但是不一定能請求到那麼多。據體還得看返回的數量

 */ 

struct v4l2_requestbuffers req;

memset (&req,0,sizeof(req));

req.count = 10;

req.type    = V4L2_BUF_TYPE_VIDEO_CAPTURE;

req.memory  = V4L2_MEMORY_MMAP;

if (-1==ioctl(fd,VIDIOC_REQBUFS,&req)){

perror ("While requesting buffers");

return -3;

}

if (req.count < 5){

fprintf (stderr, "Can't get enough buffers!/n");

return -4;

}

 

/* 這裏請求了10塊緩存區,並將其類型設爲MMAP型。 */

 

/* 獲取緩衝區的信息 

 * 在操作之前,我們必須要能記錄下我們

 * 申請的緩存區,並在最後使用munmap釋放它們

 * 這裏使用結構體

 * struct buffer {

 *      void * start; 

 *   ssize_t length;

 * } 以及buffer數量

 * static int nbuffer

 * 來表示

 */

struct buffer * buffers = (struct buffer *)malloc (nbuffer*sizeof(*buffers));

if (!buffers){

perror ("Can't allocate memory for buffers!");

return -4;

}

 

struct v4l2_buffer buf;

for (nbuffer=0;nbuffer<req.count;++nbuffer) {

memset (&buf,0,sizeof(buf));

buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

buf.memory = V4L2_MEMORY_MMAP;

buf.index   = nbuffer;

 

if (-1==ioctl(fd,VIDIOC_QUERYBUF,&buf)){

perror ("While querying buffer");

return -5;

}

 

buffers[nbuffer].length = buf.length;

buffers[nbuffer].start = mmap (

NULL, 

buf.length,

PROT_READ,  /* 官方文檔說要加上PROT_WRITE,但加上會出錯 */

MAP_SHARED,

fd,

buf.m.offset

);

 

if (MAP_FAILED == buffers[nbuffer].start) {

perror ("While mapping memory");

return -6;

}

}

 

/*這個循環完成後,所有緩存區都保存在

 *了buffers這個數組裏了,完了就再將它們munmap即可。

 */

 

/* 打開視頻捕獲 */

enum v4l2_buf_type type;

type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

if (-1==ioctl(fd,VIDIOC_STREAMON,&type)){

perror ("While opening stream");

return -7;

}

 

/* 與內核交換緩衝區 */

unsigned int i;

i=0;

while(1) {

memset (&buf,0,sizeof(buf));

buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

buf.memory = V4L2_MEMORY_MMAP;

buf.index = i;

 

if (-1==ioctl(fd,VIDIOC_DQBUF,&buf)){

perror ("While getting buffer's data");

return -8;

}

 

/* 現在就得到了一片緩衝區的數據,發送處理 */

process_image ( buffers+buf.index,buf.index );

 

/* 將緩衝區交還給內核 */

if (-1==ioctl(fd,VIDIOC_QBUF,&buf)){

perror ("While returning buffer's data");

return -9;

}

 

i = (i+1) & nbuffer;

}

這就是所有獲取的過程了。至於圖像的處理,則是由解碼函數和Qt來處理。現在先進行一些思路的設計。設想在進行圖像的轉換時,必須提供一塊內存區域來進行,我們當然可以在轉換時使用malloc來進行動態分配,轉換完成並顯示後,再將它free。然而這樣做對內核而言是一個不小的負擔:每次爲一整張圖片分配內存,最少也有上百KB,如此大的分配量和釋放量,很容易造成內存碎片加重內核的負擔。由於僅是每轉換一次才顯示一次圖像,所以這片用於轉換的內存區域可以安全地複用,不會同時由兩個線程操作之。因此在初始化時我們爲每一塊內存映射緩衝區分配一塊內存區域作爲轉換用。對於MJPEG到JPEG的轉換,使用相同的內存大小。代碼就不在此列出了。這片假設這個內存區域的起始指針爲convertion_buffers,在process_image (struct buffer * buf, int index ) 中,有

void process_image (struct buffer *buf, int index){

struct * buffer conv_buf = convertion_buffers+index;

do_image_conversion (

buf->start, buf->length,  /* 要轉換的區域 */

conv_buf->start, conv_buf->length, /* 保存轉換數據的區域 */

);

 

/* 現在就可以把數據取出並交給QPixmap處理 

* 要在一個QWidget裏作圖,必須重載paintEvent 

* 函數並用QPainter作畫。然而paintEvent

* 是由事件驅動層調用的,我們不能手工,

* 所以在我們自己的的重載類裏要保存一個全局 

* 的QPixmap。這裏設爲 QPixmap * m_pixmap

*/

m_pixmap -> loadFromData (conv_buf->start,conv_buf->length);

/* 立即安排一次重繪事件 */

repaint (); 

}

 

/* 重載的paintEvent示例 */

MyWidget::paintEvent (QPaintEvent * evt) {

   QPainter painter(this);

   painter.drawPixmap (QPoint(0,0),*m_pixmap);

   QWidget::paintEvent(evt);

}

這裏講Pixmap畫到了(0,0)處。

考慮的改進之處:

 

雖然上述程序已經可以工作了,但是有一些細節可以改進。比如圖像轉換之處,可能相當耗時。解決的辦法之一可以考慮多線程,用一個線程進行數據的收集,每收集一幀數據便通知顯示的進程。顯示的進程使用一個FIFO收集數據,用一個定時器,在固定的時間到時,然後從FIFO中取出數據進行轉換然後顯示。兩個線程互不干擾,可以更有效地利用CPU,使收集、轉換和顯示協調地工作。

VN:F [1.9.6_1107]

 

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