1. 程式人生 > >Linux QT攝像頭採集影象

Linux QT攝像頭採集影象

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;
 }

 // 放入快取佇列
    if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
       return -1;
    } }
 
/*這個迴圈完成後,所有快取區都儲存在
 *了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,使收集、轉換和顯示協調地工作。

轉載自網路。

作者原來基礎上修改:在程式碼斷 if (MAP_FAILED == buffers[nbuffer].start) {
  perror ("While mapping memory");
  return -6;
 }

後加入如下程式碼,否則沒有資料:

// 放入快取佇列
    if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
       return -1;
    }