最近在做類似於飛思卡爾的項目,要用到攝像頭,攝像頭接在一塊樹莓派上,但樹莓派上跑的是Linux系統。因為對Linux並不熟悉,身邊也沒有比較熟悉Linux的朋友,幾次想嘗試最終都因為遇到大多問題找不原因放棄了。這次又開始學習Linux,同樣遇到一堆的問題,但硬著頭皮,一個一個的找資料解決。
Video for Linuxtwo(Video4Linux2)簡稱V4L2,是V4L的改進版。V4L2是linux操作系統下用於采集圖片、視頻和音頻數據的API接口,配合適當的視頻采集設備和相應的驅動程序,可以實現圖片、視頻、音頻等的采集。在遠程會議、可視電話、視頻監控系統和嵌入式多媒體終端中都有廣泛的應用。
要剛接觸到V4L2,有太多的東西要學。以下大部是摘抄前輩博客或者網上其它地方找到的自認為有幫助的內容,記錄下來。
一、首先熟悉需要用到的函數
ioctl函數說明
ioctl是設備驅動程序中對設備的I/O通道進行管理的函數。所謂對I/O通道進行管理,就是對設備的一些特性進行控制,例如串口的傳輸波特率、馬達的轉速等等。
用戶程序可以使用ioctl函數通過命令碼(cmd)來實現控制功能,至於怎麼解釋這些命令和怎麼實現這些命令,這都是驅動程序要做的事情。而在驅動程序中實現的ioctl函數體內,有一個switch{case}結構,每一個case對應一個命令碼,做出一些相應的操作,怎麼實現這些操作,這是每一個程序員自己的事情。
頭文件:
#inlcude
函數定義:
int ioctl(int fd, ind cmd, …);
返回值:
成功返回0,出錯返回-1且errno設為某特定值
參數:
fd:open()返回的文件描述符
cmd:是用戶程序對設備的控制命令
…:省略號表示補充參數,一般最多一個,有或沒有是和cmd的意義相關的。
cmd介紹:
控制命令很多,這裡只介紹圖像采集時用到的命令
VIDIOC_QUERYCAP /* 獲取設備支持的操作 */
VIDIOC_G_FMT /* 獲取設置支持的視頻格式 */
VIDIOC_S_FMT /* 設置捕獲視頻的格式 */
VIDIOC_REQBUFS /* 向驅動提出申請內存的請求 */
VIDIOC_QUERYBUF /* 向驅動查詢申請到的內存 */
VIDIOC_QBUF /* 將空閒的內存加入可捕獲視頻的隊列 */
VIDIOC_DQBUF /* 將已經捕獲好視頻的內存拉出已捕獲視頻的隊列 */
VIDIOC_STREAMON /* 打開視頻流 */
VIDIOC_STREAMOFF /* 關閉視頻流 */
VIDIOC_QUERYCTRL /* 查詢驅動是否支持該命令 */
VIDIOC_G_CTRL /* 獲取當前命令值 */
VIDIOC_S_CTRL /* 設置新的命令值 */
VIDIOC_G_TUNER /* 獲取調諧器信息 */
VIDIOC_S_TUNER /* 設置調諧器信息 */
VIDIOC_G_FREQUENCY /* 獲取調諧器頻率 */
VIDIOC_S_FREQUENCY /* 設置調諧器頻率 */
open函數說明
頭文件:
#include
函數定義:
int open(const char *pathname,int oflag, ...)
返回值:
成功返回最小的未被使用的文件描述符,否則返回-1;
參數說明:
pathname:待打開或創建文件的路徑名。
oflag:指定文件的打開或創建模式,打開或創建文件時,至少使用下述三個常量中的一個。
O_RDONLY 只讀模式
O_WRONLY 只寫模式
O_RDWR 讀寫模式
以下常量是選用的:
O_APPEND 每次寫操作都寫入文件的末尾。
O_CREAT 如果指定文件不存在,則創建這個文件。
O_EXCL 如果要創建的文件已存在,則返回-1,並且修改errno的值。
O_TRUNC 如果文件存在,並且以只寫或讀寫方式打開,則清空文件全部內容。
O_NOCTTY 如果路徑名指向終端設備,不要把這個設備用作控制終端。
O_NONBLOCK 如果路徑名指向指向FIFO/塊文件/字符文件,則把文件的打開和後繼I/O設置為非阻塞模式。使用非阻塞模式調用視頻設備,即使尚未捕獲到信息,驅動依舊會把緩存(DQBUFF)裡的內容返回給應用程序。
以下三個常量同樣是選用的,它們用於同步輸入輸出。
O_SDYNC 等待物理I/O結束後再write。在不影響讀取新寫入的數據的前提下,不等待文件屬性更新。
O_RSYNC read等待所有寫入丗一區域的寫操作完成後再進行。
O_SYNC 等待物理I/O結束後再write,包括更新文件屬性的I/O。
mmap函數說明
頭文件:
#include
函數定義:
void *mmap(void *start, size_t length, int prot, int flags,int fd, off_t offset);
返回值:
執行成功返回被映射區的指針, 執行失敗返回MAP_FAILED[其值為(void *)-1]
EACCES:訪問出錯
EAGAIN:文件已被鎖定,或者太多的內存已被鎖定
EBADF:fd不是有效的文件描述詞
EINVAL:一個或者多個參數無效
ENFILE:已達到系統對打開文件的限制
ENODEV:指定文件所在的文件系統不支持內存映射
ENOMEM:內存不足,或者進程已超出最大內存映射數量
EPERM:權能不足,操作不允許
ETXTBSY:已寫的方式打開文件,同時指定MAP_DENYWRITE標志
SIGSEGV:試著向只讀區寫入
SIGBUS:試著訪問不屬於進程的內存區
參數:
start:映射區的開始地址。
length:映射區的長度。
prot:期望的內存保護標志,不能與文件的打開模式沖突。是以下的某個值,可以通過or
PROT_EXEC //頁內容可以被執行
PROT_READ //頁內容可以被讀取
PROT_WRITE //頁可以被寫入
PROT_NONE //頁不可訪問
flags:指定映射對象的類型,映射選項和映射頁是否可以共享。它的值可以是一個或者多個以下位的組合體
MAP_FIXED //使用指定的映射起始地址,如果由start和len參數指定的內存區重疊於現存的映射空間,重疊部分將會被丟棄。如果指定的起始地址不可用,操作將會失敗。並且起始地址必須落在頁的邊界上。
MAP_SHARED //與其它所有映射這個對象的進程共享映射空間。對共享區的寫入,相當於輸出到文件。直到msync()或者munmap()被調用,文件實際上不會被更新。
MAP_PRIVATE //建立一個寫入時拷貝的私有映射。內存區域的寫入不會影響到原文件。這個標志和以上標志是互斥的,只能使用其中一個。
MAP_DENYWRITE //這個標志被忽略。
MAP_EXECUTABLE //同上
MAP_NORESERVE //不要為這個映射保留交換空間。當交換空間被保留,對映射區修改的可能會得到保證。當交換空間不被保留,同時內存不足,對映射區的修改會引起段違例信號。
MAP_LOCKED //鎖定映射區的頁面,從而防止頁面被交換出內存。
MAP_GROWSDOWN //用於堆棧,告訴內核VM系統,映射區可以向下擴展。
MAP_ANONYMOUS //匿名映射,映射區不與任何文件關聯。
MAP_ANON //MAP_ANONYMOUS的別稱,不再被使用。
MAP_FILE //兼容標志,被忽略。
MAP_32BIT //將映射區放在進程地址空間的低2GB,MAP_FIXED指定時會被忽略。當前這個標志只在x86-64平台上得到支持。
MAP_POPULATE //為文件映射通過預讀的方式准備好頁表。隨後對映射區的訪問不會被頁違例阻塞。
MAP_NONBLOCK //僅和MAP_POPULATE一起使用時才有意義。不執行預讀,只為已存在於內存中的頁面建立頁表入口。
fd:有效的文件描述詞。如果MAP_ANONYMOUS被設定,為了兼容問題,其值應為-1。
offset:被映射對象內容的起點。
v4l2_capability結構
ioctl (fd, VIDIOC_QUERYCAP, &cap)
命令通過結構 v4l2_capability 獲取設備支持的操作模式:
struct v4l2_capability
{
__u8 driver[16]; //驅動名。
__u8 card[32]; // Device名
__u8 bus_info[32]; //在Bus系統中存放位置
__u32 version; //driver 版本
__u32 capabilities; //能力集
__u32 reserved[4];
};
v4l2_format結構
struct v4l2_format
{
enum v4l2_buf_type type;
union
{
struct v4l2_pix_format pix;
struct v4l2_window win;
struct v4l2_vbi_format vbi;
struct v4l2_sliced_vbi_format sliced;
__u8 raw_data[200];
} fmt;
};
enum v4l2_buf_type
{
V4L2_BUF_TYPE_VIDEO_CAPTURE = 1, //視頻捕捉模式
V4L2_BUF_TYPE_VIDEO_OUTPUT = 2,
V4L2_BUF_TYPE_VIDEO_OVERLAY = 3,
...
V4L2_BUF_TYPE_PRIVATE = 0x80,
};
v4l2_pix_format結構
struct v4l2_pix_format
{
__u32 width; // 寬,必須是16的倍數
__u32 height; // 高,必須是16的倍數
__u32 pixelformat; // 視頻數據格式(常見的值有 V4L2_PIX_FMT_YUV422P | V4L2_PIX_FMT_RGB565)
enum v4l2_field field; //掃描方式,(逐行掃描,隔行掃描)
__u32 bytesperline; //一行圖像占用的字節數
__u32 sizeimage; //圖像占用的總字節數
enum v4l2_colorspace colorspace; //顏色空間
__u32 priv; // private data, depends on pixelformat
};
v4l2_requestbuffers結構
struct v4l2_requestbuffers
{
__u32 count; // 緩存數量,也就是說在緩存隊列裡保持多少張照片
enum v4l2_buf_type type; // 數據流類型,必須永遠是V4L2_BUF_TYPE_VIDEO_CAPTURE
enum v4l2_memory memory; // V4L2_MEMORY_MMAP 或V4L2_MEMORY_USERPTR
__u32 reserved[2];
};
關於視頻采集方式
操作系統一般把系統使用的內存劃分成用戶空間和內核空間,分別由應用程序管理和操作系統管理。應用程序可以直接訪問內存的地址,而內核空間存放的是 供內核訪問的代碼和數據,用戶不能直接訪問。v4l2捕獲的數據,最初是存放在內核空間的,這意味著用戶不能直接訪問該段內存,必須通過某些手段來轉換地 址。
一共有三種視頻采集方式:使用read、write方式;內存映射方式和用戶指針模式。
read、write方式:在用戶空間和內核空間不斷拷貝數據,占用了大量用戶內存空間,效率不高。
內存映射方式:把設備裡的內存映射到應用程序中的內存控件,直接處理設備內存,這是一種有效的方式。上面的mmap函數就是使用這種方式。
用戶指針模式:內存片段由應用程序自己分配。這點需要在v4l2_requestbuffers裡將memory字段設置成V4L2_MEMORY_USERPTR。
處理采集數據
V4L2有一個數據緩存,存放req.count數量的緩存數據。數據緩存采用FIFO的方式,當應用程序調用緩存數據時,緩存隊列將最先采集到的 視頻數據緩存送出,並重新采集一張視頻數據。這個過程需要用到兩個ioctl命令,VIDIOC_DQBUF和VIDIOC_QBUF:
struct v4l2_buffer buf;
memset(&buf,0,sizeof(buf));
buf.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory=V4L2_MEMORY_MMAP;
buf.index=0;
//讀取緩存
if (ioctl(cameraFd, VIDIOC_DQBUF, &buf) == -1)
{
return -1;
}
//Deal_Frame(...) //處理幀數據
//重新放入緩存隊列
if (ioctl(cameraFd, VIDIOC_QBUF, &buf) == -1)
{
return -1;
}
二、編程思路
在Linux下,所有外設都被看成一種特殊的文件,成為“設備文件”,可以象訪問普通文件一樣對其進行讀寫。一般來說,采用V4L2驅動的攝像頭設備文件是/dev/video0。V4L2支持兩種方式來采集圖像:內存映射方式(mmap)和直接讀取方式(read)。V4L2在include/linux/videodev.h文件中定義了一些重要的數據結構,在采集圖像的過程中,就是通過對這些數據的操作來獲得最終的圖像數據。Linux系統V4L2的能力可在Linux內核編譯階段配置,默認情況下都有此開發接口。
而攝像頭所用的主要是capature了,視頻的捕捉,具體linux的調用可以參考下圖。
vcq9tcTK08a1ssm8r6GjPC9wPg0KPHA+06bTw7PM0PLNqLn9VjRMMr3Tv9qyybyvytPGtcr9vt231s6qzuW49rK91uijujwvcD4NCjxwPjxzdHJvbmc+MaGiPC9zdHJvbmc+tPK/qsrTxrXJ6LG4zsS8/qOsvfjQ0MrTxrWyybyvtcSyzsr9s/XKvLuvo6zNqLn9VjRMMr3Tv9rJ6NbDytPGtc28z/G1xLLJvK+0sL/aoaKyybyvtcS149XztPPQobrNuPHKvTs8L3A+DQo8cD48c3Ryb25nPjKhojwvc3Ryb25nPsnqx+vI9LjJytPGtbLJvK+1xNahu7qz5cf4o6yyor2r1eLQqdahu7qz5cf4tNPE2rrLv9W85NOzyeS1vdPDu6e/1bzko6yx49Pa06bTw7PM0PK2wcihL7SmwO3K08a1yv2+3Ts8L3A+DQo8cD48c3Ryb25nPjOhojwvc3Ryb25nPr2ryerH67W9tcTWobu6s+XH+NTaytPGtbLJvK/K5MjrttPB0MXFttOjrLKixvS2r8rTxrWyybyvOzwvcD4NCjxwPjxzdHJvbmc+NKGiPC9zdHJvbmc+x/22r7+qyrzK08a1yv2+3bXEssm8r6Os06bTw7PM0PK008rTxrWyybyvyuSz9rbTwdDIobP21qG7urPlx/ijrLSmwO3N6rrzo6y9q9ahu7qz5cf41tjQwrfFyOvK08a1ssm8r8rkyOu208HQo6zRrbu3zfm4tLLJvK/BrND4tcTK08a1yv2+3Ts8L3A+DQo8cD48c3Ryb25nPjWhojwvc3Ryb25nPs2j1rnK08a1ssm8r6GjPC9wPg0KPHA+vt/M5bXEs8zQ8sq1z9bB97PMv8nS1LLOv7zPwsPmtcTB97PMzbw6PGJyIC8+DQo8aW1nIGFsdD0="這裡寫圖片描述" src="http://www.2cto.com/uploadfile/Collfiles/20160618/20160618090524445.png" title="\" />
其實其他的都比較簡單,就是通過ioctl這個接口去設置一些參數。最主要的就是buf管理。他有一個或者多個輸入隊列和輸出隊列。
啟動視頻采集後,驅動程序開始采集一幀數據,把采集的數據放入視頻采集輸入隊列的第一個幀緩沖區,一幀數據采集完成,也就是第一個幀緩沖區存滿一幀數據後,驅動程序將該幀緩沖區移至視頻采集輸出隊列,等待應用程序從輸出隊列取出。驅動程序接下來采集下一幀數據,放入第二個幀緩沖區,同樣幀緩沖區存滿下一幀數據後,被放入視頻采集輸出隊列。
應用程序從視頻采集輸出隊列中取出含有視頻數據的幀緩沖區,處理幀緩沖區中的視頻數據,如存儲或壓縮。
最後,應用程序將處理完數據的幀緩沖區重新放入視頻采集輸入隊列,這樣可以循環采集,如圖所示。
每一個幀緩沖區都有一個對應的狀態標志變量,其中每一個比特代表一個狀態
V4L2_BUF_FLAG_UNMAPPED 0B0000
V4L2_BUF_FLAG_MAPPED 0B0001
V4L2_BUF_FLAG_ENQUEUED 0B0010
V4L2_BUF_FLAG_DONE 0B0100
緩沖區的狀態轉化如圖所示。
三、程序代碼
運行環境:
系統: ubuntu-14.04
OPENCV: opencv-2.4.9
開發平台: Qt5.5.1
攝像頭: Microsoft_LifeCam_Studio http://detail.zol.com.cn/webcams/index257694.shtml
/****************************/
/*V4L2視頻采集測試程序 */
/****************************/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define CLEAR(x) memset(&(x), 0, sizeof(x));
struct buffer
{
void *start;
size_t length;
};
static char *dev_name = "/dev/video0";
int main(int argc, char **argv)
{
/* 非阻塞式打開攝像頭設備 */
int fd = open(dev_name, O_RDWR | O_NONBLOCK, 0);
if (fd < 0)
{
perror("Can't open device");
exit(EXIT_FAILURE);
}
/* 查詢視頻設備支持格式 */
struct v4l2_fmtdesc fmtdesc;
CLEAR(fmtdesc);
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
printf("Support format:\n");
while (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) == 0)
{
fmtdesc.index++;
printf("pixelformat = ''%c%c%c%c''\ndescription = ''%s''\n", fmtdesc.pixelformat & 0xFF, (fmtdesc.pixelformat >> 8) & 0xFF, (fmtdesc.pixelformat >> 16) & 0xFF, (fmtdesc.pixelformat >> 24) & 0xFF, fmtdesc.description);
}
/* 查詢設備的輸出格式並打印輸出 */
struct v4l2_format fmt;
CLEAR(fmt);
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;// 設置視頻捕獲模式
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_FIELD_INTERLACED; // 隔行掃描的方式
/* 設置圖像格式 */
ioctl(fd, VIDIOC_S_FMT, &fmt);
/* 申請視頻緩沖區 */
struct v4l2_requestbuffers req_buf;
CLEAR(req_buf);
req_buf.count = 10; // 緩沖區大小
req_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req_buf.memory = V4L2_MEMORY_MMAP; // 緩沖區類型設置為MMAP型
if (-1 == ioctl(fd,VIDIOC_REQBUFS,&req_buf))
{
perror("While requestion buffers\n");
exit(EXIT_FAILURE);
}
if (req_buf.count < 5)
{
fprintf(stderr,"Can't get enough buffers!\n");
exit(EXIT_FAILURE);
}
unsigned int nbuffer = req_buf.count;
/* 申請用戶緩沖區 */
struct buffer *usr_buf = (struct buffer *)calloc(nbuffer, sizeof(*usr_buf));
if (!usr_buf)
{
perror("Can't allocate memory for usr_buf\n");
exit(EXIT_FAILURE);
}
/* 將申請到的幀緩沖映射到用戶空間 */
for (nbuffer = 0; nbuffer < req_buf.count; ++nbuffer)
{
struct v4l2_buffer buf;
CLEAR(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = nbuffer;
// 查詢序號為nbuffer的緩沖區,得到其起始物理地址和大小
if (-1 == ioctl(fd,VIDIOC_QUERYBUF,&buf))
{
perror("While querying buffer");
exit(EXIT_FAILURE);
}
// 映射內存到用戶空間
usr_buf[nbuffer].length = buf.length;
usr_buf[nbuffer].start = mmap(
NULL,
buf.length,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd,
buf.m.offset
);
if (MAP_FAILED == usr_buf[nbuffer].start)
{
perror("While mapping memory");
exit(EXIT_FAILURE);
}
// 將申請到的幀緩沖放入緩存隊列
if (-1 == ioctl(fd, VIDIOC_QBUF, &buf))
{
return -1;
}
}
/* 打開采集視頻 */
enum v4l2_buf_type type;
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (-1 == ioctl(fd, VIDIOC_STREAMON, &type))
{
perror("While opening stream");
exit(EXIT_FAILURE);
}
printf("\nVideo stream on\n\n");
/* 與內核交換緩沖區 */
unsigned int i = 0;
cvNamedWindow("Capture",CV_WINDOW_AUTOSIZE);
// 這一段涉及到異步IO
while (true)
{
fd_set fds;
struct timeval tv;
int r;
// 將指定的文件描述符集清空
FD_ZERO(&fds);
// 在文件描述符集合中增加一個新的文件描述符
FD_SET(fd, &fds);
/*Timeout.*/
tv.tv_sec = 2;
tv.tv_usec = 0;
// 判斷是否可讀(即攝像頭是否准備好),tv是定時
r = select(fd + 1, &fds, NULL, NULL, &tv);
if (-1 == r)
{
if( EINTR == errno)
{
continue;
}
printf("select err\n");
}
if (0 == r)
{
fprintf(stderr,"select timeout\n");
break;
}
struct v4l2_buffer buf;
CLEAR(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 buffers data");
break;
}
/* 將幀內容賦值給CvMat格式的數據 */
CvMat cvmat=cvMat(fmt.fmt.pix.height,fmt.fmt.pix.width,CV_8UC3,usr_buf[buf.index].start);
/* 解碼,這一步將數據轉換為IplImage格式 */
IplImage *img =cvDecodeImage(&cvmat, 1);
cvShowImage("Capture",img);
cvWaitKey(30); //注意這裡要加時延
cvReleaseImage(&img);
/* 把用用完的緩沖幀放回隊列 */
if (-1 == ioctl(fd, VIDIOC_QBUF, &buf))
{
perror("While returning buffers data");
break;
}
i = (i+1) & nbuffer;
}
/* 關閉視頻流 */
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (-1 == ioctl(fd,VIDIOC_STREAMOFF,&type))
{
perror("STREAMOFF fail\n");
exit(EXIT_FAILURE);
}
/* 斷開映射 */
for (unsigned int i = 0; i < nbuffer; i++)
{
if (-1 == munmap(usr_buf[i].start, usr_buf[i].length))
{
exit(-1);
}
}
free(usr_buf);
/* 關閉設備 */
if (-1 == close(fd))
{
perror("Fail to close fd");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
四、運行結果
今天下午采集640x480成功,但剛才運行發現還有問題,不知道為什麼,正在找原因。