1、本文所提及的攝像頭不是zc0301p,使用的API不是V4L,顯示所使用的上位機不是QT,特此說明。
2、UVC只是一個驅動,如果它能成功驅動攝像頭,會在/dev目錄下出現video(或video0、video1等等)。這樣,就可以使用Linux提供(準確說不是Linux提供,具體的百度吧)的一套API,即V4L2來訪問攝像頭了。“使用”是指我們寫的採集數據的程序,而不是指“攝像頭驅動程序”。——驅動程序不是那麼簡單就能寫出來的。
3、本文未涉及大量的如VIDIOC_S_FMT等命令字,也不涉及V4L2採集模型,一來網絡上很多這種說明(原創的,轉載的),本文不想偷竊他人的成果;二來諸君如果看V4L2手冊、看源代碼的話,會學得更多,——如果肯下心思的話。
4、源代碼中本來就沒有中文註釋,抄於此,也不再添加中文了。
5、本文所述的程序是根據開源項目luvcview寫的,理所當然的,本文的程序也會公開源代碼,使用條款爲GPL。至於這個程序是好是壞,就由衆人去評說了。
1、驅動——UVC
在Linux中,除了SPCA和GSPCA這類經典的USB攝像頭驅動外,還有一種,即Linux UVC,全稱爲Linux USB Video Class,從Class這個詞可以看出,UVC是代碼某一類的視頻設備驅動,官網上的說法包括了webcams, digital camcorders, analog video converters, analog 以及 digital television tuners等等。從2.6.26版本開始,Linux UVC驅動就納入到內核中,不需要手動下載。但是需要自己手動配置內核,纔可使用UVC。
具體的測試示例,可以在這個網址上找到:
http://blog.chinaunix.net/u1/58951/showart_2199263.html
2、採集——V4L2
在Linux下,視頻數據的採集有兩套API,分別爲V4L和V4L2。是Video For Linux的兩個版本。其實在Windows下也有一套API,名爲Video For Windows,即VFW,具體怎麼使用,我沒研究過,不過,按Windows的習俗,應該不難。
本文所用的API爲V4L2,雖然它的第一個版本也可以使用,但是爲了表明筆者與時俱進的精神,決心使用第二個版本。這個版本無外就下面三點:
1、打開或關閉攝像頭設備都是調用POSIX標準的open或close,很簡單;
2、最重要、最核心的是使用ioctl調用,通過不同的控制命令來控制攝像頭,如開始捕獲、停止捕獲、獲取攝像頭信息,等等。如果研究過Linux驅動的話,對ioctl應該不陌生。
3、一些結構體,如保存攝像頭信息的,捕獲攝像頭數據的緩衝區的,等等。
攝像頭的信息保存於自定義的結構體video_info中。如下:
struct video_info
{
int camfd; /**< camera file descriptor */
struct v4l2_capability cap;
struct v4l2_format fmt;
struct v4l2_requestbuffers rb;
struct v4l2_buffer buf;
enum v4l2_buf_type type;
void* mem[NB_BUFFER]; /**< main buffers */
uint8* tmp_buffer; /**< for MJPEG */
uint8* frame_buffer; /**< one frame buffer here */
uint32 frame_size_in;
uint32 format; /**< eg YUYV or MJPEG,etc. */
int width;
int height;
int is_streaming; /**< start capture */
int is_quit;
#ifdef DEBUG
enum v4l2_field field;
uint32 bytes_per_line;
uint32 size_image;
enum v4l2_colorspace color_space;
uint32 priv;
#endif
};
在開始採集數據前,需要先看一下攝像頭的信息,下面三個函數完成的功能分別爲得到攝像頭的屬性(capability),得到攝像頭的格式(format),設置攝像頭的格式。屬性包括了驅動信息,總線信息,是否支持流捕獲等等;格式包括了攝像頭數據格式(如MJPEG、YUYV等等),圖像的寬、高等等。
int v4l2_get_capability(struct video_info* vd_info);
int v4l2_get_format(struct video_info* vd_info);
int v4l2_set_foramt(struct video_info* vd_info,
uint32 width, uint32 height,uint32 format);
圖1爲在FC系統和ARM開發板上得到的攝像頭信息。從圖中可以清楚看到上述所講的各種信息。至於最後一行的錯誤提示,是由於我調用v4l2_set_foramt將攝像頭數據格式設置爲YUYV出錯了,即它不支持YUYV格式。然而,當我使用這個程序在紅旗操作系統下測試時,結果又不同了,它是YUYV格式了!我將它設置爲MJPEG格式,同樣不行,所以圖2最後同樣出錯。(那時正興高采烈地做畢業設計,這個問題讓我足足鬱悶了好幾天。我想不通是什麼原因)
圖1 攝像頭信息
下面簡單講一下程序片段,具體的程序,參見附錄中。
(1)、分配內存
switch (vd_info->format) /**< format will be also ok */
{
case V4L2_PIX_FMT_MJPEG:
vd_info->tmp_buffer =
(uint8 *)calloc(1, (size_t)vd_info->frame_size_in);
if (vd_info->tmp_buffer == NULL)
error_out("unable alloc tmp_buffer");
vd_info->frame_buffer =
(uint8 *)calloc(1, (size_t)vd_info->width * (vd_info->height+8) * 2);
if (vd_info->frame_buffer == NULL)
error_out("unable alloc frame_buffer");
break;
case V4L2_PIX_FMT_YUYV:
vd_info->frame_buffer =
(uint8 *)calloc(1,(size_t)vd_info->frame_size_in);
if (vd_info->frame_buffer == NULL)
error_out("unable alloc frame_buffer");
break;
default:
msg_out("error!\n");
return -1;
break;
}
因爲YUYV是一種原始數據,可以直接顯示,不需要編解碼,而MJPEG格式的,需要解碼,所以分要分配兩個緩衝區。
(2)、打開,O_NONBLOCK是以非阻塞方式打開。
vd_info->camfd = open(device, O_RDWR /*| O_NONBLOCK*/, 0);
if (vd_info->camfd < 0)
error_out("can not open the device");
(3)、查詢
if (-1 == ioctl(vd_info->camfd, VIDIOC_QUERYCAP, &vd_info->cap))
error_out("query camera failed");
if (0 == (vd_info->cap.capabilities & V4L2_CAP_VIDEO_CAPTURE))
{
debug_msg("video capture not supported.\n");
return -1;
}
(4)、設置格式
memset(&vd_info->fmt, 0, sizeof(struct v4l2_format));
vd_info->fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vd_info->fmt.fmt.pix.width = width;
vd_info->fmt.fmt.pix.height = height;
vd_info->fmt.fmt.pix.field =V4L2_FIELD_ANY;
vd_info->fmt.fmt.pix.pixelformat = format;
if (-1 == ioctl(vd_info->camfd, VIDIOC_S_FMT, &vd_info->fmt))
error_out("unable to set format ");
(5)、查詢緩衝區、映射到用戶空間內存
memset(&vd_info->rb, 0, sizeof(struct v4l2_requestbuffers));
vd_info->rb.count = NB_BUFFER; /**< 4 buffers */
vd_info->rb.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vd_info->rb.memory = V4L2_MEMORY_MMAP;
if (-1 == ioctl(vd_info->camfd, VIDIOC_REQBUFS, &vd_info->rb))
error_out("unable to allocte buffers");
/* map the buffers(4 buffer) */
for (i = 0; i < NB_BUFFER; i++)
{
memset(&vd_info->buf, 0, sizeof(struct v4l2_buffer));
vd_info->buf.index = i;
vd_info->buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vd_info->buf.memory = V4L2_MEMORY_MMAP;
if (-1 == ioctl(vd_info->camfd, VIDIOC_QUERYBUF, &vd_info->buf))
error_out("unable to query buffer");
/* map it, 0 means anywhere */
vd_info->mem[i] =
mmap(0, vd_info->buf.length, PROT_READ, MAP_SHARED,
vd_info->camfd, vd_info->buf.m.offset);
/* MAP_FAILED = (void *)-1 */
if (MAP_FAILED == vd_info->mem[i])
error_out("unable to map buffer");
}
(6)、進入隊列
/* queue the buffers */
for (i = 0; i < NB_BUFFER; i++)
{
memset(&vd_info->buf, 0, sizeof(struct v4l2_buffer));
vd_info->buf.index = i;
vd_info->buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vd_info->buf.memory = V4L2_MEMORY_MMAP;
if (-1 == ioctl(vd_info->camfd, VIDIOC_QBUF, &vd_info->buf))
error_out("unable to queue the buffers");
}
(7)、開始捕獲(發出捕獲信號)
vd_info->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (-1 == ioctl(vd_info->camfd, VIDIOC_STREAMON, &vd_info->type))
error_out("unable to start capture");
vd_info->is_streaming = 1;
debug_msg("stream on OK!!\n");
debug_msg("===============================\n\n");
(8)、採集(從緩衝區隊列中取出數據,再將數據的內存複製另一內存區)
static int count = 0;
if (!vd_info->is_streaming) /**< if stream is off, start it */
{
if (v4l2_on(vd_info)) /**< failed */
goto err;
}
memset(&vd_info->buf, 0, sizeof(struct v4l2_buffer));
vd_info->buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vd_info->buf.memory = V4L2_MEMORY_MMAP;
/* get data from buffers */
if (-1 == ioctl(vd_info->camfd, VIDIOC_DQBUF, &vd_info->buf))
{
msg_out("unable to dequeue buffer\n");
goto err;
}
switch (vd_info->format)
{
case V4L2_PIX_FMT_MJPEG:
if (vd_info->buf.bytesused <= HEADFRAME1)
{
msg_out("ignore empty frame...\n");
return 0;
}
/* we can save tmp_buff to a jpg file,just write it! */
memcpy(vd_info->tmp_buffer, vd_info->mem[vd_info->buf.index],
vd_info->buf.bytesused);
/* here decode MJPEG,so we can dispaly it */
if (jpeg_decode(&vd_info->frame_buffer, vd_info->tmp_buffer,
&vd_info->width, &vd_info->height) < 0 )
{
msg_out("decode jpeg error\n");
goto err;
}
break;
case V4L2_PIX_FMT_YUYV:
if (vd_info->buf.bytesused > vd_info->frame_size_in)
memcpy(vd_info->frame_buffer, vd_info->mem[vd_info->buf.index],
(size_t)vd_info->frame_size_in);
else
memcpy(vd_info->frame_buffer, vd_info->mem[vd_info->buf.index],
(size_t)vd_info->buf.bytesused);
break;
default:
goto err;
break;
}
/* here you can process the frame! */
v4l2_process(vd_info);
/* queue buffer again */
if (-1 == ioctl(vd_info->camfd, VIDIOC_QBUF, &vd_info->buf))
{
fprintf(stderr,"requeue error\n");
goto err;
}
debug_msg("frame:%d\n", count++);
debug_msg("frame size in: %d KB\n", vd_info->frame_size_in>>10);
return 0;
err:
vd_info->is_quit = 0;
return -1;
(9)、停止捕獲(發送停止信號)
vd_info->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (-1 == ioctl(vd_info->camfd, VIDIOC_STREAMOFF, &vd_info->type))
error_out("unable to stop capture");
vd_info->is_streaming = 0;
debug_msg("stream off OK!\n");
debug_msg("===============================\n\n");
(10)、關閉設備(釋放內存、關閉設備)
uint16 i = 0;
if (vd_info->is_streaming) /**< stop if it is still capturing */
v4l2_off(vd_info);
if (vd_info->frame_buffer)
free(vd_info->frame_buffer);
if (vd_info->tmp_buffer)
free(vd_info->tmp_buffer);
vd_info->frame_buffer = NULL;
/* it is a good thing to unmap! */
for (i = 0; i < NB_BUFFER; i++)
{
if (-1 == munmap(vd_info->mem[i], vd_info->buf.length))
error_out("munmap");
}
close(vd_info->camfd);
debug_msg("close OK!\n");
3、顯示——SDL
SDL是Simple DirectMedia Layer的簡稱,是一個自由的跨平臺的多媒體開發包,適用於遊戲、遊戲SDK、演示軟件、模擬器、MPEG播放器和其他應用軟件。本文將它大材小用,用於顯示採集得到的視頻數據。
顯示的代碼片段如下:
SDL_Surface *pscreen = NULL;
SDL_Overlay *overlay = NULL;
SDL_Rect drect;
SDL_Event sdlevent;
SDL_mutex *affmutex = NULL;
unsigned char frmrate;
unsigned char *p = NULL;
uint32 currtime;
uint32 lasttime;
char* status = NULL;
/************* Test SDL capabilities ************/
/* memory leak here */
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
fprintf(stderr, "Couldn't initialize SDL: %s\n", SDL_GetError());
exit(1);
}
/* it need to alloc space! */
vd_info = (struct video_info *) calloc(1, sizeof(struct video_info));
/* init the camera,you can change the last three params!!! */
if (v4l2_init(vd_info, V4L2_PIX_FMT_YUYV, 640, 480) < 0)
return EXIT_FAILURE;
pscreen =
SDL_SetVideoMode(vd_info->width, vd_info->height, 0,
SDL_VIDEO_Flags);
overlay =
SDL_CreateYUVOverlay(vd_info->width, vd_info->height,
SDL_YUY2_OVERLAY, pscreen);
/* here?? */
p = (unsigned char *) overlay->pixels[0];
drect.x = 0;
drect.y = 0;
drect.w = pscreen->w;
drect.h = pscreen->h;
lasttime = SDL_GetTicks();
affmutex = SDL_CreateMutex();
/* big loop */
while (vd_info->is_quit)
{
while (SDL_PollEvent(&sdlevent))
{
if (sdlevent.type == SDL_QUIT)
{
vd_info->is_quit = 0;
break;
}
}
currtime = SDL_GetTicks();
if (currtime - lasttime > 0)
{
frmrate = 1000/(currtime - lasttime);
}
lasttime = currtime;
if (v4l2_grab(vd_info) < 0)
{
printf("Error grabbing \n");
break;
}
SDL_LockYUVOverlay(overlay);
/* frame_buffer to p */
memcpy(p, vd_info->frame_buffer,
vd_info->width * (vd_info->height) * 2);
SDL_UnlockYUVOverlay(overlay);
SDL_DisplayYUVOverlay(overlay, &drect); /* dispaly it */
status = (char *)calloc(1, 20*sizeof(char));
sprintf(status, "come on fps:%d",frmrate);
SDL_WM_SetCaption(status, NULL);
SDL_Delay(10);
}
SDL_DestroyMutex(affmutex);
SDL_FreeYUVOverlay(overlay);
v4l2_close(vd_info);
free(status);
free(vd_info);
printf("Clean Up done Quit \n");
SDL_Quit();
return 0;
在紅旗操作系統下測試如圖3所示。最上面的即爲SDL繪製的窗口(至於它向下調用什麼圖形庫就不用理會了),中間的爲控制檯,它不斷輸出採集到的幀數以及每一幀的大小。最下面的是emacs。
爲了演示emacs的多窗口gdb調試,現附上幾個圖。算是emacs篇的補充吧。
附圖1中,已經加載了可執行文件,並設置了斷點(命令爲b main emacs,單步後,中間窗口出現紅點即爲斷點處)。
附圖2所示的是正在顯示圖像,左上角即爲控制檯,出現的信息同上述截圖一樣。
附圖3爲結束的情形。重新回到gdb等待命令狀態。
此時的emacs五個窗口中各有自己的功能,從上而下,分別爲控制檯、變量、源代碼、幀棧以及斷點。
看到了吧?emacs中調試不比VC差吧?
附圖1 開始
附圖2 顯示
附圖3 結束
UVC官網:
http://www.ideasonboard.org/uvc/
V4L2官網:
http://linux.bytesex.org/v4l2/
本文的程序下載:
camera-pc-latelee.org.tar.bz2
luvcview項目下載:
luvcview-20070512.tar.bz2
V4L2手冊地址:
http://v4l2spec.bytesex.org/v4l2spec/v4l2.pdf