video4linux(v4l)使用攝像頭的實例基礎教程與體會


先介紹幾本V4L的書籍和文章,《基於Video4Linux 的USB 攝像頭圖像採集實現》,陳俊宏寫的《video stream 初探》的一系列共六篇文章,也找了一些英文的資料,看到過《video4linux programming》但是這篇文章偏重於視頻設備在linux中的驅動實現,《Video4Linux Kernel API Reference》詳細介紹了v4l中各個重要的結構體的作用。另外順着陳俊宏的文章,找到了一個叫EffecTV的軟件,其中的有關v4l的源碼部分也很值得一看,在後的文章裏也會介紹。翻看了網上的很多文章,多半是使用陳俊宏介紹的相關代碼,或者是EffecTV中的,大家都是這麼用而且也都用的不錯。

  三個大部分:

第一個部分介紹一些v4l的基本概念和基本方法,利用系統API完成一系列函數以方便後續應用程序的開發和使用。

第二個部分一些說明如何使用v4l,用一個示例程序說明。

第三個部分想簡單說一說對獲取和處理圖像相關問題的思路。在這一章可能會談一談我的一些理解和體會。其實網絡上的資料很多,我只是稍微整理一下而已。

   

我的感覺linux內核和驅動開發的那些程序員很厲害因爲他們留給我們一個很容易使用的接口而使底層複雜的工作對我們很透明,讀過上述我提到的文章後會覺得使用v4l是相對容易的(我希望如果有人讀了我的文章也會有這種感覺),相對複雜的是採集到圖像數據後我們應該怎麼辦,我想這也可能是很多人當然也包括我所不是特別清晰和明確的。所以我想在第三個部分裏做一些對採集到圖像數據後相關問題的探討,當然我的水平有限,請您指出文中的錯誤方法和對概念的錯誤理解,我非常願意共同學習和進步。

1.video4linux基礎相關

1.1 v4l的介紹與一些基礎知識的介紹

I.首先說明一下video4linux(v4l)

它是一些視頻系統,視頻軟件,音頻軟件的基礎,經常使用在需要採集圖像的場合,如視頻監控,webcam,可視電話,經常應用在embedded linux中是linux嵌入式開發中經常使用的系統接口。它是linux內核提供給用戶空間的編程接口,各種的視頻和音頻設備開發相應的驅動程序後,就可以通過v4l提供的系統API來控制視頻和音頻設備,也就是說v4l分爲兩層,底層爲音視頻設備在內核中的驅動,上層爲系統提供的API,而對於我們來說需要的就是使用這些系統的API。

II.Linux系統中的文件操作

有關Linux系統中的文件操作不屬於本文的內容。但是還是要了解相關係統調用的作用和使用方法。其中包括open()read()close()ioctl()mmap()。詳細的使用不作說明。在Linux系統中各種設備(當然包括視頻設備)也都是用文件的形式來使用的。他們存在與dev目錄下,所以本質上說,在Linux中各種外設的使用(如果它們已經正確的被驅動),與文件操作本質上是沒有什麼區別的。

1.2 建立一套簡單的v4l函數庫

       這一節將一邊介紹v4l的使用方法,一邊建立一套簡單的函數,應該說是一套很基本的函數,它完成很基本的夠能但足夠展示如何使用v4l。這些函數可以用來被其他程序使用,封裝基本的v4l功能。本文只介紹一些和攝像頭相關的編程方法,並且是最基礎最簡單的,所以一些內容並沒有介紹,一些與其他視頻設備(如視頻採集卡)和音頻設備有關的內容也沒有介紹,本人也不是很理解這方面的內容。

       這裏先給出接下來將要開發出來函數的一個總覽。

相關結構體和函數的定義我們就放到一個名爲v4l.h的文件中,相關函數的編寫就放在一個名爲v4l.c的文件中把。

對於這個函數庫共有如下的定義(也就是大體v4l.h中的內容):

#ifndef _V4L_H_

#define _V4L_H_

#include <sys/types.h>

#include <linux/videodev.h> //使用v4l必須包含的頭文件

這個頭文件可以在/usr/include/linux下找到,裏面包含了對v4l各種結構的定義,以及各種ioctl的使用方法,所以在下文中有關v4l的相關結構體並不做詳細的介紹,可以參看此文件就會得到你想要的內容。

下面是定義的結構體,和相關函數,突然給出這麼多的代碼很唐突,不過隨着一點點解釋條理就會很清晰了。

struct _v4l_struct

      {

         int fd;//保存打開視頻文件的設備描述符

         struct video_capability capability;//該結構及下面的結構爲v4l所定義可在上述頭文件中找到

         struct video_picture picture;

         struct video_mmap mmap;

         struct video_mbuf mbuf;

         unsigned char *map;//用於指向圖像數據的指針

               int frame_current;

         int frame_using[VIDEO_MAXFRAME];//這兩個變量用於雙緩衝在後面介紹。

      };

typedef struct _v4l_struct v4l_device;

//上面的定義的結構體,有的文中章有定義channel的變量,但對於攝像頭來說設置這個變量意義不大通常只有一個channel,本文不是爲了寫出一個大而全且成熟的函數庫,只是爲了介紹如何使用v4l,再加上本人水平也有限,能夠給讀者一個路線我就很知足了,所以並沒有設置這個變量同時與channel相關的函數也沒有給出。

 

extern int v4l_open(char *, v4l_device *);

extern int v4l_close(v4l_device *);

extern int v4l_get_capability(v4l_device *);

extern int v4l_get_picture(v4l_device *);

extern int v4l_get_mbuf(v4l_device *);

extern int v4l_set_picture(v4l_device *, int, int, int, int, int,);

extern int v4l_grab_picture(v4l_device *, unsigned int);

extern int v4l_mmap_init(v4l_device *);

extern int v4l_grab_init(v4l_device *, int, int);

extern int v4l_grab_frame(v4l_device *, int);

extern int v4l_grab_sync(v4l_device *);

上述函數會在下文中逐漸完成,功能也會逐漸介紹,雖然現在看起來沒什麼感覺只能從函數名上依稀體會它的功能,或許看起來很煩,不過看完下文就會好了。

 

前面已經說過使用v4l視頻編程的流程和對文件操作並沒有什麼本質的不同,大概的流程如下:

       1.打開視頻設備(通常是/dev/video0)

       2.獲得設備信息。

       3.根據需要更改設備的相關設置。

       4.獲得採集到的圖像數據(在這裏v4l提供了兩種方式,直接通過打開的設備讀取數據,使用mmap內存映射的方式獲取數據)。

       5.對採集到的數據進行操作(如顯示到屏幕,圖像處理,存儲成圖片文件)。

       6.關閉視頻設備。

知道了流程之後,我們就需要根據流程完成相應的函數。

 

那麼我們首先完成第1步打開視頻設備,需要完成int v4l_open(char *, v4l_device *);

具體的函數如下

#define DEFAULT_DEVICE “/dev/video0”

int v4l_open(char *dev , v4l_device *vd)

{

       if(!dev)dev= DEFAULT_DEVICE;

       if((vd-fd=open(dev,O_RDWR))<0){perror(“v4l_open:”);return -1;}

       if(v4l_get_capability(vd))return -1;

       if(v4l_get_picture(vd))return -1;//這兩個函數就是即將要完成的獲取設備信息的函數

       return 0

}

同樣對於第6步也十分簡單,就是int v4l_close(v4l_device *);的作用。

函數如下:

int v4l_close(v4l_device *vd)

{close(vd->fd);return 0;}

現在我們完成第2步中獲得設備信息的任務,下面先給出函數在對函數作出相應的說明。

int v4l_get_capability(v4l_device *vd)

{  

   if (ioctl(vd->fd, VIDIOCGCAP, &(vd->capability)) < 0) {  

      perror("v4l_get_capability:");  

      return -1;  

   }  

   return 0;  

}

int v4l_get_picture(v4l_device *vd)  

{  

   if (ioctl(vd->fd, VIDIOCGPICT, &(vd->picture)) < 0) {  

      perror("v4l_get_picture:");  

      return -1;  

   }  

   return 0;  

}

對於以上兩個函數我們不熟悉的地方可有vd->capability和vd->picture兩個結構體,和這兩個函數中最主要的語句ioctl。對於ioctl的行爲它是由驅動程序提供和定義的,在這裏當然是由v4l所定義的,其中宏VIDIOCGCAP和VIDIOCGPICT的分別表示獲得視頻設備的capability和picture。對於其他的宏功能定義可以在你的Linux系統中的/usr/include/linux/videodev.h中找到,這個頭文件也包含了capability和picture的定義。例如:

struct video_capability

{

       char name[32];

       int type;

       int channels;   /* Num channels */

       int audios;      /* Num audio devices */

       int maxwidth; /* Supported width */

       int maxheight; /* And height */

       int minwidth;  /* Supported width */

       int minheight; /* And height */

};capability結構它包括了視頻設備的名稱,頻道數,音頻設備數,支持的最大最小寬度和高度等信息。

struct video_picture

{

       __u16     brightness;

       __u16     hue;

       __u16     colour;

       __u16     contrast;

       __u16     whiteness;       /* Black and white only */

       __u16     depth;            /* Capture depth */

       __u16   palette;    /* Palette in use */

}picture結構包括了亮度,對比度,色深,調色板等等信息。頭文件裏還列出了palette相關的值,這裏並沒有給出。

       瞭解了以上也就瞭解了這兩個簡單函數的作用,現在我們已經獲取到了相關視頻設備的capabilty和picture屬性。

這裏直接給出另外一個函數

int v4l_get_mbuf(v4l_device *vd)  

{  

   if (ioctl(vd->fd, VIDIOCGMBUG ,&(vd->mbuf)) < 0) {  

      perror("v4l_get_mbuf:");  

      return -1;  

   }  

   return 0;  

}

對於結構體video_mbuf在v4l中的定義如下,video_mbuf結構體是爲了服務使用mmap內存映射來獲取圖像的方法而設置的結構體,通過這個結構體可以獲得攝像頭設備存儲圖像的內存大小。具體的定義如下,各變量的使用也會在下文詳細說明。

struct video_mbuf

{

       int   size;        可映射的攝像頭內存大小

       int   frames;    攝像頭可同時存儲的幀數

       int   offsets[VIDEO_MAX_FRAME];每一幀圖像的偏移量

};

       下面完成第3步按照需要更改設備的相應設置,事實上可以更改的設置很多,本文以更改picture屬性爲例說明更改屬性的一般方法。

       那麼我們就完成extern int v4l_set_picture(v4l_device *, int, int, int, int, int,);這個函數吧

int v4l_set_picture(v4l_device *vd,int br,int hue,int col,int cont,int white)

{

   if(br) vd->picture.brightnesss=br;

   if(hue) vd->picture.hue=hue;

   if(col) vd->picture.color=col;

   if(cont) vd->picture.contrast=cont;

   if(white) vd->picture.whiteness=white;

   if(ioctl(vd->fd,VIDIOCSPICT,&(vd->picture))<0)

   {perror("v4l_set_picture: ");return -1;}  

   return 0;

}

上述函數就是更改picture相關屬性的例子,其核心還是v4l給我們提供的ioctl的相關調用,通過這個函數可以修改如亮度,對比度等相關的值。

       第4步獲得採集到的圖像數據。

       這一步是使用v4l比較重要的一步,涉及到幾個函數的編寫。當然使用v4l就是爲了要獲得圖像,所以這一步很關鍵,但是當你獲得了圖像數據後,還需要根據你想要達到的目的和具體情況做進一步的處理,也就是第5步所做的事情,這些內容將在後面第三部分提到。這裏講如何獲得採集到的數據。

       如前所述獲得圖像的方式有兩種,分別是直接讀取設備使用mmap內存映射,而通常大家使用的方法都是後者。

1).直接讀取設備

直接讀設備的方式就是使用read()函數,我們先前定義的

extern int v4l_grab_picture(v4l_device *, unsigned int);函數就是完成這個工作的,它的實現也很簡單。

 

int v4l_grab_picture(v4l_device *vd, unsighed int size)

{

   if(read(vd-fd,&(vd->map),size)==0)return -1;

   return 0;

}

該函數的使用也很簡單,就是給出圖像數據的大小,vd->map所指向的數據就是圖像數據。而圖像數據的大小你要根據設備的屬性自己計算獲得。

2).使用mmap內存映射來獲取圖像

       在這部分涉及到下面幾個函數,它們配合來完成最終圖像採集的功能。

       extern int v4l_mmap_init(v4l_device *);該函數把攝像頭圖像數據映射到進程內存中,也就是隻要使用vd->map指針就可以使用採集到的圖像數據(下文詳細說明)

extern int v4l_grab_init(v4l_device *, int, int);該函數完成圖像採集前的初始化工作。

extern int v4l_grab_frame(v4l_device *, int);該函數是真正完成圖像採集的一步,在本文使用了一個通常都會使用的一個小技巧,可以在處理一幀數據時同時採集下一幀的數據,因爲通常我們使用的攝像頭都可以至少存儲兩幀的數據。

extern int v4l_grab_sync(v4l_device *);該函數用來完成截取圖像的同步工作,在截取一幀圖像後調用,返回表明一幀截取結束。

 

       下面分別介紹這幾個函數。

 

       mmap()系統調用使得進程之間通過映射同一個普通文件實現共享內存。普通文件被映射到進程地址空間後,進程可以像訪問普通內存一樣對文件進行訪問,不必在調用read(),write()等操作。兩個不同進程A、B共享內存的意思是,同一塊物理內存被映射到進程A、B各自的進程地址空間。進程A可以即時訪問進程B對共享內存中數據的更新,反之亦然。

       採用共享內存通信的一個顯而易見的好處是減少I/O操作提高讀取效率,因爲使用mmap後進程可以直接讀取內存而不需要任何數據的拷貝。

mmap的函數原型如下

void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )

addr:共享內存的起始地址,一般設爲0,表示由系統分配。

len:指定映射內存的大小。在我們這裏,該值爲攝像頭mbuf結構體的size值,即圖像數據的總大小。

port:指定共享內存的訪問權限 PROT_READ(可讀),PROT_WRITE(可寫)

flags:一般設置爲MAP_SHARED

fd:同享文件的文件描述符。

 

介紹完了mmap的使用,就可以介紹上文中定義的函數extern int v4l_mmap_init(v4l_device *);了。先給出這個函數的代碼,再做說明。

int v4l_mmap_init(v4l_device *vd)  

{  

   if (v4l_get_mbuf(vd) < 0)  

   return -1;  

   if ((vd->map = mmap(0, vd->mbuf.size, PROT_READ|PROT_WRITE, MAP_SHARED, vd->fd, 0)) < 0) {  

      perror("v4l_mmap_init:mmap");  

      return -1;  

   }  

   return 0;  

}

這個函數首先使用v4l_get_mbuf(vd)獲得一個攝像頭重要的參數,就是需要映射內存的大小,即vd->mbuf.size,然後調用mmap,當我們在編程是調用v4l_mmap_init後,vd.map指針所指向的內存空間即爲我們將要採集的圖像數據。

      

      獲得圖像前的初始化工作v4l_grab_init();該函數十分簡單直接粘上去,其中將。vd->frame_using[0]和vd->frame_using[1]都設爲FALSE,表示兩幀的截取都沒有開始。

int v4l_grab_init(v4l_device *vd, int width, int height)  

{  

   vd->mmap.width = width;   

   vd->mmap.height = height;   

   vd->mmap.format = vd->picture.palette;   

   vd->frame_current = 0;  

   vd->frame_using[0] = FALSE;  

   vd->frame_using[1] = FALSE;  

  

   return v4l_grab_frame(vd, 0);  

       真正獲得圖像的函數extern int v4l_grab_frame(v4l_device *, int);

int v4l_grab_frame(v4l_device *vd, int frame)  

{  

   if (vd->frame_using[frame]) {  

      fprintf(stderr, "v4l_grab_frame: frame %d is already used./n", frame);  

      return -1;  

   }  

  

   vd->mmap.frame = frame;  

   if (ioctl(vd->fd, VIDIOCMCAPTURE, &(vd->mmap)) < 0) {  

      perror("v4l_grab_frame");  

      return -1;  

   }  

   vd->frame_using[frame] = TRUE;  

   vd->frame_current = frame;  

   return 0;  

}  

讀到這裏,應該覺得這個函數也是相當的簡單。最關鍵的一步即爲調用ioctl(vd->fd, VIDIOCMCAPTURE, &(vd->mmap)),調用後相應的圖像就已經獲取完畢。其他的代碼是爲了完成雙緩衝就是截取兩幀圖像用的,可以自己理解下。

       在截取圖像後還要進行同步操作,就是調用extern int v4l_grab_sync(v4l_device *);函數,該函數如下

int v4l_grab_sync(v4l_device *vd)  

{  

   if (ioctl(vd->fd, VIDIOCSYNC, &(vd->frame_current)) < 0) {  

      perror("v4l_grab_sync");  

   }  

   vd->frame_using[vd->frame_current] = FALSE;  

   return 0;  

}  

該函數返回0說明你想要獲取的圖像幀已經獲取完畢。

圖像存在了哪裏?

       最終我們使用v4l的目的是爲了獲取設備中的圖像,那麼圖像存在哪裏?從上面的文章可以知道,vd.map指針所指就是你要獲得的第一幀圖像。圖像的位置,存在vd.map+vd.mbuf.offsets[vd.frame_current]處。其中vd.frame_current=0,即爲第一幀的位置,vd.frame_current=1,爲第二幀的位置。

 

2 上述v4l庫使用的方法

給出了上述的一些代碼,這裏用一些簡單的代碼表明如何來使用它。上文中已經說過將相關結構體和函數的定義放到一個名爲v4l.h的文件中,相關函數的編寫放在一個名爲v4l.c的文件。

現在我們要使用它們。

使用的方法很簡單,你創建一個.c文件,假設叫test.c吧,那麼test.c如下

//test.c

include “v4l.h”

...

v4l_device vd;

 

void main()

{

       v4l_open(DEFAULT_DEVICE,&vd);

       v4l_mmap_init(&vd);

       v4l_grab_init(&vd,320,240);

       v4l_grab_sync(&vd);//此時就已經獲得了一幀的圖像,存在vd.map中

       while(1)

       {

              vd.frame_current ^= 1;  

              v4l_grab_frame(&vd, vd.frame_current);

              v4l_grab_sync(&vd);

              圖像處理函數(vd.map+vd. vd.map+vd.mbuf.offsets[vd.frame_current]);

              //循環採集,調用你設計的圖像處理函數來處理圖像

//其中vd.map+vd. vd.map+vd.mbuf.offsets[vd.frame_current]就是圖像所在位置。

}

}

 

3 有關獲取的圖像的一些問題

:我獲取到的圖像究竟長什麼樣?

:每個攝像頭獲取的圖像數據的格式可能都不盡相同,可以通過picture. palette獲得。獲得的圖像有黑白的,有yuv格式的,RGB格式的,也有直接爲jpeg格式的。你要根據實際情況,和你的需要對圖像進行處理。比如常見的,如果你要在嵌入式的LCD上顯示假設LCD是RGB24的,但是你獲得圖像是YUV格式的那麼你就將他轉換爲RGB24的。具體的轉換方法可以上網查找,也可參考前面提到過的effectTV中的相關代碼。

 

:如何顯示圖像或將圖像保存?

:假設你採集到的圖像爲RGB24格式的,我接觸過的可以使用SDL庫顯示(網絡上很流行的叫spcaview的軟件就是這樣的,不過它將圖像數據壓縮爲jpeg的格式後顯示,這個軟件也被經常的移植到一些嵌入式平臺使用,如ARM的)。當然也可以使用嵌入式linux的Framebuffer直接寫屏顯示。將圖像保存可以用libjpeg將其保存爲jpeg圖片直接存儲,相關的使用方法可以上網查找。也可以使用一些視頻編碼,將其編碼保存(我希望學習一下相關的技術因爲我對這方面一點不懂,如果你有一些資料可以推薦給我看,我十分想看一看)。

 

       一邊寫文章一邊才發現自己很菜,因爲很多都是參考別人的文章,而自己想寫出來去一落鍵盤就寫不出什麼。就寫這麼多,因爲我只會這麼多。高手見笑,新手和我一樣我們互相討論

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