硬件平台:Digilent ZedBoard + USB 摄像头
开发环境:Windows XP 32 bit + Wmare 8.0 + Ubuntu 10.04 + arm-linux-xilinx-gnueabi交叉编译环境
Zedboard linux: Digilent OOB Design
一、一些知识
1、V4L和V4L2。
V4L是Linux环境下开发视频采集设备驱动程序的一套规范(API),它为驱动程序的编写提供统一的接口,并将所有的视频采集设备的驱动程序都纳入其的管理之中。V4L不仅给驱动程序编写者带来极大的方便,同时也方便了应用程序的编写和移植。V4L2是V4L的升级版,由于我们使用的OOB是3.3的内核,不再支持V4L,因而编程不再考虑V4L的api和参数定义。
2、YUYV与RGB24
RGB是一种颜色的表示法,计算机中一般采用24位来存储,每个颜色占8位。YUV也是一种颜色空间,为什么要出现YUV,主要有两个原因,一个是为了让彩色信号兼容黑白电视机,另外一个原因是为了减少传输的带宽。YUV中,Y表示亮度,U和V表示色度,总之它是将RGB信号进行了一种处理,根据人对亮度更敏感些,增加亮度的信号,减少颜色的信号,以这样“欺骗”人的眼睛的手段来节省空间。YUV到RGB颜色空间转换关系是:
R = Y + 1.042*(V-128); G = Y - 0.34414*(U-128) - 0.71414*(V-128); B = Y + 1.772*(U-128);
YUV的格式也很多,不过常见的就是422、420等。YUYV就是422形式,简单来说就是,两个像素点P1、P2本应该有Y1、U1、V1和Y2、U2、V2这六个分量,但是实际只保留Y1、U1、Y2、V2。
图1 YUYV像素
二、应用程序设计
先定义一些宏和结构体,方便后续编程
1 #define TRUE 1 2 #define FALSE 0 3 4 #define FILE_VIDEO "/dev/video0" 5 #define BMP "/usr/image_bmp.bmp" 6 #define YUV "/usr/image_yuv.yuv" 7 8 #define IMAGEWIDTH 640 9 #define IMAGEHEIGHT 480 10 11 static int fd; 12 static struct v4l2_capability cap; 13 struct v4l2_fmtdesc fmtdesc; 14 struct v4l2_format fmt,fmtack; 15 struct v4l2_streamparm setfps; 16 struct v4l2_requestbuffers req; 17 struct v4l2_buffer buf; 18 enum v4l2_buf_type type; 19 unsigned char frame_buffer[IMAGEWIDTH*IMAGEHEIGHT*3];
其中
#define FILE_VIDEO "/dev/video0"
是要访问的摄像头设备,默人都是/dev/video0
#define BMP "/usr/image_bmp.bmp" #define YUV "/usr/image_yuv.yuv"
是采集后存储的图片,为了方便测试,这里将直接获取的yuv格式数据也保存成文件,可以通过yuvviewer等查看器查看。
static int fd; static struct v4l2_capability cap; struct v4l2_fmtdesc fmtdesc; struct v4l2_format fmt,fmtack; struct v4l2_streamparm setfps; struct v4l2_requestbuffers req; struct v4l2_buffer buf; enum v4l2_buf_type type;
这些结构体的定义都可以从/usr/include/linux/videodev2.h中找到定义,具体含义在后续编程会做相应解释。
#define IMAGEWIDTH 640 #define IMAGEHEIGHT 480
为采集图像的大小。
定义一个frame_buffer,用来缓存RGB颜色数据
unsigned char frame_buffer[IMAGEWIDTH*IMAGEHEIGHT*3]
这些宏和定义结束后,就可以开始编程配置摄像头并采集图像了。一般来说V4L2采集视频数据分为五个步骤:首先,打开视频设备文件,进行视频采集的参数初始化,通过V4L2接口设置视频图像的采集窗口、采集的点阵大小和格式;其次,申请若干视频采集的帧缓冲区,并将这些帧缓冲区从内核空间映射到用户空间,便于应用程序读取/处理视频数据;第三,将申请到的帧缓冲区在视频采集输入队列排队,并启动视频采集;第四,驱动开始视频数据的采集,应用程序从视频采集输出队列取出帧缓冲区,处理完后,将帧缓冲区重新放入视频采集输入队列,循环往复采集连续的视频数据;第五,停止视频采集。在本次设计中,定义了三个函数实现对摄像头的配置和采集。
int init_v4l2(void); int v4l2_grab(void); int close_v4l2(void);
同时由于采集到的图像数据是YUYV格式,需要进行颜色空间转换,定义了转换函数。
int yuyv_2_rgb888(void);
下面就详细介绍这几个函数的实现。
1、初始化V4l2
(1)打开视频。linux对摄像头的访问和普通设备一样,使用open函数就可以,返回值是设备的id。
1 if ((fd = open(FILE_VIDEO, O_RDWR)) == -1) 2 { 3 printf("Error opening V4L interface\n"); 4 return (FALSE); 5 }
(2)读video_capability中信息。通过调用IOCTL函数和接口命令VIDIOC_QUERYCAP查询摄像头的信息,结构体v4l2_capability中有包括驱动名称driver、card、bus_info、version以及属性capabilities。这里我们需要检查一下是否是为视频采集设备V4L2_CAP_VIDEO_CAPTURE以及是否支持流IO操作V4L2_CAP_STREAMING。
1 if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) 2 { 3 printf("Error opening device %s: unable to query device.\n",FILE_VIDEO); 4 return (FALSE); 5 } 6 else 7 { 8 printf("driver:\t\t%s\n",cap.driver); 9 printf("card:\t\t%s\n",cap.card); 10 printf("bus_info:\t%s\n",cap.bus_info); 11 printf("version:\t%d\n",cap.version); 12 printf("capabilities:\t%x\n",cap.capabilities); 13 14 if ((cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) == V4L2_CAP_VIDEO_CAPTURE) 15 { 16 printf("Device %s: supports capture.\n",FILE_VIDEO); 17 } 18 19 if ((cap.capabilities & V4L2_CAP_STREAMING) == V4L2_CAP_STREAMING) 20 { 21 printf("Device %s: supports streaming.\n",FILE_VIDEO); 22 } 23 }
(3)列举摄像头所支持像素格式。使用命令VIDIOC_ENUM_FMT,获取到的信息通过结构体v4l2_fmtdesc查询。这步很关键,不同的摄像头可能支持的格式不一样,V4L2可以支持的格式很多,/usr/include/linux/videodev2.h文件中可以看到。
1 fmtdesc.index=0; 2 fmtdesc.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; 3 printf("Support format:\n"); 4 while(ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc)!=-1) 5 { 6 printf("\t%d.%s\n",fmtdesc.index+1,fmtdesc.description); 7 fmtdesc.index++; 8 }
(4)设置像素格式。一般的USB摄像头都会支持YUYV,有些还支持其他的格式。通过前一步对摄像头所支持像素格式查询,下面需要对格式进行设置。命令为VIDIOC_S_FMT,通过结构体v4l2_format把图像的像素格式设置为V4L2_PIX_FMT_YUYV,高度和宽度设置为IMAGEHEIGHT和IMAGEWIDTH。一般情况下一个摄像头所支持的格式是不可以随便更改的,我尝试把把一个只支持YUYV和MJPEG的摄像头格式改为RGB24或者JPEG,都没有成功。
1 fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 2 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; 3 fmt.fmt.pix.height = IMAGEHEIGHT; 4 fmt.fmt.pix.width = IMAGEWIDTH; 5 fmt.fmt.pix.field = V4L2_FIELD_INTERLACED; 6 7 if(ioctl(fd, VIDIOC_S_FMT, &fmt) == -1) 8 { 9 printf("Unable to set format\n"); 10 return FALSE; 11 }
为了确保设置的格式作用到摄像头上,再通过命令VIDIOC_G_FMT将摄像头设置读取回来。
1 if(ioctl(fd, VIDIOC_G_FMT, &fmt) == -1) 2 { 3 printf("Unable to get format\n"); 4 return FALSE; 5 } 6 { 7 printf("fmt.type:\t\t%d\n",fmt.type); 8 printf("pix.pixelformat:\t%c%c%c%c\n",fmt.fmt.pix.pixelformat & 0xFF, (fmt.fmt.pix.pixelformat >> 8) & 0xFF,(fmt.fmt.pix.pixelformat >> 16) & 0xFF, (fmt.fmt.pix.pixelformat >> 24) & 0xFF); 9 printf("pix.height:\t\t%d\n",fmt.fmt.pix.height); 10 printf("pix.width:\t\t%d\n",fmt.fmt.pix.width); 11 printf("pix.field:\t\t%d\n",fmt.fmt.pix.field); 12 }
完整的初始化代码如下:
1 int init_v4l2(void) 2 { 3 int i; 4 int ret = 0; 5 6 //opendev 7 if ((fd = open(FILE_VIDEO, O_RDWR)) == -1) 8 { 9 printf("Error opening V4L interface\n"); 10 return (FALSE); 11 } 12 13 //query cap 14 if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) 15 { 16 printf("Error opening device %s: unable to query device.\n",FILE_VIDEO); 17 return (FALSE); 18 } 19 else 20 { 21 printf("driver:\t\t%s\n",cap.driver); 22 printf("card:\t\t%s\n",cap.card); 23 printf("bus_info:\t%s\n",cap.bus_info); 24 printf("version:\t%d\n",cap.version); 25 printf("capabilities:\t%x\n",cap.capabilities); 26 27 if ((cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) == V4L2_CAP_VIDEO_CAPTURE) 28 { 29 printf("Device %s: supports capture.\n",FILE_VIDEO); 30 } 31 32 if ((cap.capabilities & V4L2_CAP_STREAMING) == V4L2_CAP_STREAMING) 33 { 34 printf("Device %s: supports streaming.\n",FILE_VIDEO); 35 } 36 } 37 38 //emu all support fmt 39 fmtdesc.index=0; 40 fmtdesc.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; 41 printf("Support format:\n"); 42 while(ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc)!=-1) 43 { 44 printf("\t%d.%s\n",fmtdesc.index+1,fmtdesc.description); 45 fmtdesc.index++; 46 } 47 48 //set fmt 49 fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 50 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; 51 fmt.fmt.pix.height = IMAGEHEIGHT; 52 fmt.fmt.pix.width = IMAGEWIDTH; 53 fmt.fmt.pix.field = V4L2_FIELD_INTERLACED; 54 55 if(ioctl(fd, VIDIOC_S_FMT, &fmt) == -1) 56 { 57 printf("Unable to set format\n"); 58 return FALSE; 59 } 60 if(ioctl(fd, VIDIOC_G_FMT, &fmt) == -1) 61 { 62 printf("Unable to get format\n"); 63 return FALSE; 64 } 65 { 66 printf("fmt.type:\t\t%d\n",fmt.type); 67 printf("pix.pixelformat:\t%c%c%c%c\n",fmt.fmt.pix.pixelformat & 0xFF, (fmt.fmt.pix.pixelformat >> 8) & 0xFF,(fmt.fmt.pix.pixelformat >> 16) & 0xFF, (fmt.fmt.pix.pixelformat >> 24) & 0xFF); 68 printf("pix.height:\t\t%d\n",fmt.fmt.pix.height); 69 printf("pix.width:\t\t%d\n",fmt.fmt.pix.width); 70 printf("pix.field:\t\t%d\n",fmt.fmt.pix.field); 71 } 72 //set fps 73 setfps.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 74 setfps.parm.capture.timeperframe.numerator = 10; 75 setfps.parm.capture.timeperframe.denominator = 10; 76 77 printf("init %s \t[OK]\n",FILE_VIDEO); 78 79 return TRUE; 80 }
2、图像采集
(1)申请缓存区。使用参数VIDIOC_REQBUFS和结构体v4l2_requestbuffers。v4l2_requestbuffers结构中定义了缓存的数量,系统会据此申请对应数量的视频缓存。
1 req.count=4; 2 req.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; 3 req.memory=V4L2_MEMORY_MMAP; 4 if(ioctl(fd,VIDIOC_REQBUFS,&req)==-1) 5 { 6 printf("request for buffers error\n"); 7 8 }
(2)获取每个缓存的信息,并mmap到用户空间。定义结构体
struct buffer { void * start; unsigned int length; } * buffers;
来存储mmap后的地址信息。需要说明的是由于mmap函数定义时返回的地址是个void *,因而这里面的start也是个 void *。实际地址在运行的时候会自动分配。
1 for (n_buffers = 0; n_buffers < req.count; n_buffers++) 2 { 3 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 4 buf.memory = V4L2_MEMORY_MMAP; 5 buf.index = n_buffers; 6 //query buffers 7 if (ioctl (fd, VIDIOC_QUERYBUF, &buf) == -1) 8 { 9 printf("query buffer error\n"); 10 return(FALSE); 11 } 12 13 buffers[n_buffers].length = buf.length; 14 //map 15 buffers[n_buffers].start = mmap(NULL,buf.length,PROT_READ |PROT_WRITE, MAP_SHARED, fd, buf.m.offset); 16 if (buffers[n_buffers].start == MAP_FAILED) 17 { 18 printf("buffer map error\n"); 19 return(FALSE); 20 } 21 }
(3) 之后就可以开始采集视频了。使用命令VIDIOC_STREAMON。
1 type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 2 ioctl (fd, VIDIOC_STREAMON, &type);
(4)取出缓存中已经采样的缓存。使用命令VIDIOC_DQBUF。视频数据存放的位置是buffers[n_buffers].start的地址处。
1 ioctl(fd, VIDIOC_DQBUF, &buf);
完整的采集代码:
1 int v4l2_grab(void) 2 { 3 unsigned int n_buffers; 4 5 //request for 4 buffers 6 req.count=4; 7 req.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; 8 req.memory=V4L2_MEMORY_MMAP; 9 if(ioctl(fd,VIDIOC_REQBUFS,&req)==-1) 10 { 11 printf("request for buffers error\n"); 12 } 13 14 //mmap for buffers 15 buffers = malloc(req.count*sizeof (*buffers)); 16 if (!buffers) 17 { 18 printf ("Out of memory\n"); 19 return(FALSE); 20 } 21 22 for (n_buffers = 0; n_buffers < req.count; n_buffers++) 23 { 24 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 25 buf.memory = V4L2_MEMORY_MMAP; 26 buf.index = n_buffers; 27 //query buffers 28 if (ioctl (fd, VIDIOC_QUERYBUF, &buf) == -1) 29 { 30 printf("query buffer error\n"); 31 return(FALSE); 32 } 33 34 buffers[n_buffers].length = buf.length; 35 //map 36 buffers[n_buffers].start = mmap(NULL,buf.length,PROT_READ |PROT_WRITE, MAP_SHARED, fd, buf.m.offset); 37 if (buffers[n_buffers].start == MAP_FAILED) 38 { 39 printf("buffer map error\n"); 40 return(FALSE); 41 } 42 } 43 44 //queue 45 for (n_buffers = 0; n_buffers < req.count; n_buffers++) 46 { 47 buf.index = n_buffers; 48 ioctl(fd, VIDIOC_QBUF, &buf); 49 } 50 51 type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 52 ioctl (fd, VIDIOC_STREAMON, &type); 53 54 ioctl(fd, VIDIOC_DQBUF, &buf); 55 56 printf("grab yuyv OK\n"); 57 return(TRUE); 58 }
3、YUYV转RGB24
由于摄像头采集的数据格式为YUYV,为了方便后续设计,需要转变为RGB24,并将转换完成的数据存储到frame_buffer中。值得一提的是,由于定义的时候buffers[index].start是个void *,没有办法进行+1这样的操作,需要强制转换为
char * pointer pointer = buffers[0].start
由于后续RGB的数据要存储到BMP中,而BMP文件中颜色数据是“倒序”,即从下到上,从左到右,因而在向frame_buffer写数据时是从最后一行最左测开始写,每写满一行行数减一。
1 int yuyv_2_rgb888(void) 2 { 3 int i,j; 4 unsigned char y1,y2,u,v; 5 int r1,g1,b1,r2,g2,b2; 6 char * pointer; 7 8 pointer = buffers[0].start; 9 10 for(i=0;i<480;i++) 11 { 12 for(j=0;j<320;j++) 13 { 14 y1 = *( pointer + (i*320+j)*4); 15 u = *( pointer + (i*320+j)*4 + 1); 16 y2 = *( pointer + (i*320+j)*4 + 2); 17 v = *( pointer + (i*320+j)*4 + 3); 18 19 r1 = y1 + 1.042*(v-128); 20 g1 = y1 - 0.34414*(u-128) - 0.71414*(v-128); 21 b1 = y1 + 1.772*(u-128); 22 23 r2 = y2 + 1.042*(v-128); 24 g2 = y2 - 0.34414*(u-128) - 0.71414*(v-128); 25 b2 = y2 + 1.772*(u-128); 26 27 if(r1>255) 28 r1 = 255; 29 else if(r1<0) 30 r1 = 0; 31 32 if(b1>255) 33 b1 = 255; 34 else if(b1<0) 35 b1 = 0; 36 37 if(g1>255) 38 g1 = 255; 39 else if(g1<0) 40 g1 = 0; 41 42 if(r2>255) 43 r2 = 255; 44 else if(r2<0) 45 r2 = 0; 46 47 if(b2>255) 48 b2 = 255; 49 else if(b2<0) 50 b2 = 0; 51 52 if(g2>255) 53 g2 = 255; 54 else if(g2<0) 55 g2 = 0; 56 57 *(frame_buffer + ((480-1-i)*320+j)*6 ) = (unsigned char)b1; 58 *(frame_buffer + ((480-1-i)*320+j)*6 + 1) = (unsigned char)g1; 59 *(frame_buffer + ((480-1-i)*320+j)*6 + 2) = (unsigned char)r1; 60 *(frame_buffer + ((480-1-i)*320+j)*6 + 3) = (unsigned char)b2; 61 *(frame_buffer + ((480-1-i)*320+j)*6 + 4) = (unsigned char)g2; 62 *(frame_buffer + ((480-1-i)*320+j)*6 + 5) = (unsigned char)r2; 63 } 64 } 65 printf("change to RGB OK \n"); 66 }
4、停止采集和关闭设备
使用命令VIDIOC_STREAMOFF停止视频采集,并关闭设备。
1 int close_v4l2(void) 2 { 3 ioctl(fd, VIDIOC_STREAMOFF, &buf_type); 4 if(fd != -1) 5 { 6 close(fd); 7 return (TRUE); 8 } 9 return (FALSE); 10 }
5、主函数
需要把我们采集到图像数据存储成图片,为了方便调试,先将原始的数据存储为yuv格式文件,再将转换成RGB后的数据存储为BMP。定义BMP头结构体
1 typedef struct tagBITMAPFILEHEADER{ 2 WORD bfType; // the flag of bmp, value is "BM" 3 DWORD bfSize; // size BMP file ,unit is bytes 4 DWORD bfReserved; // 0 5 DWORD bfOffBits; // must be 54 6 7 }BITMAPFILEHEADER; 8 9 10 typedef struct tagBITMAPINFOHEADER{ 11 DWORD biSize; // must be 0x28 12 DWORD biWidth; // 13 DWORD biHeight; // 14 WORD biPlanes; // must be 1 15 WORD biBitCount; // 16 DWORD biCompression; // 17 DWORD biSizeImage; // 18 DWORD biXPelsPerMeter; // 19 DWORD biYPelsPerMeter; // 20 DWORD biClrUsed; // 21 DWORD biClrImportant; // 22 }BITMAPINFOHEADER;
完整的主函数
//@超群天晴 //http://www.cnblogs.com/surpassal/ int main(void) { FILE * fp1,* fp2; BITMAPFILEHEADER bf; BITMAPINFOHEADER bi; fp1 = fopen(BMP, "wb"); if(!fp1) { printf("open "BMP"error\n"); return(FALSE); } fp2 = fopen(YUV, "wb"); if(!fp2) { printf("open "YUV"error\n"); return(FALSE); } if(init_v4l2() == FALSE) { return(FALSE); } //Set BITMAPINFOHEADER bi.biSize = 40; bi.biWidth = IMAGEWIDTH; bi.biHeight = IMAGEHEIGHT; bi.biPlanes = 1; bi.biBitCount = 24; bi.biCompression = 0; bi.biSizeImage = IMAGEWIDTH*IMAGEHEIGHT*3; bi.biXPelsPerMeter = 0; bi.biYPelsPerMeter = 0; bi.biClrUsed = 0; bi.biClrImportant = 0; //Set BITMAPFILEHEADER bf.bfType = 0x4d42; bf.bfSize = 54 + bi.biSizeImage; bf.bfReserved = 0; bf.bfOffBits = 54; v4l2_grab(); fwrite(buffers[0].start, 640*480*2, 1, fp2); printf("save "YUV"OK\n"); yuyv_2_rgb888(); fwrite(&bf, 14, 1, fp1); fwrite(&bi, 40, 1, fp1); fwrite(frame_buffer, bi.biSizeImage, 1, fp1); printf("save "BMP"OK\n"); fclose(fp1); fclose(fp2); close_v4l2(); return(TRUE); }
三、PC测试
程序编写完后,可以先在PC上做测试(实际整个调试过程都是在PC上,直道最后PC上能实现功能再挪到ZedBoard上的)。PC上测试的结果
在/usr目录下可以查看到采集到的图片
四、Zedboard测试
PC上测试OK后,可以“挪”到ZedBoard上了。使用arm-xilinx-linux交叉编译环境对源文件进行交叉编译,将生成的可执行文件拷贝到ZedBoard上运行即可。
使用命令
arm-xilinx-linux-gnueabi-gcc v4l2grab.c -o zed-camera
对程序进行编译,编译通过后将生成的可执行文件zed-camera拷贝到到ZedBoard上,并将USB摄像头连接到ZedBoard上,通过命令
ls /dev
查看dev目录下的是否有video0设备。如果有,可以运行可执行文件了。在运行前我比较习惯获得可执行文件的权限,使用命令
chmod +x zed-camera
参数+x的意思是这个文件对于当前用户是可执行的。也可以使用
chmod 777 zed-camera
这样所有用户都有读写执行的权限。使用命令
./zed-camera
执行可执行程序,程序运行,并输出以下信息:
zynq> ./zed-camera [ 318.290000] usb 1-1.3: reset high-speed USB device number 3 using xusbps-ehci driver: uvcvideo card: UVC Camera (046d:0825) bus_info: usb-xusbps-ehci.0-1.3 version: 197376 capabilities: 4000001 Device /dev/video0: supports capture. Device /dev/video0: supports streaming. Support format: 1.YUV 4:2:2 (YUYV) 2.MJPEG fmt.type: 1 pix.pixelformat: YUYV pix.height: 480 pix.width: 640 pix.field: 1 init /dev/video0 [OK] grab yuyv OK save /usr/image_yuv.yuv OK change to RGB OK save /usr/image_bmp.bmp OK
可以看到我使用的USB摄像支持YUYV和MJPEG两种格式。我也试过其他USB摄像头,大部分都只支持YUYV而不支持MJPEG或者RGB24。
采集到的图片默认是在/usr目录下的,将其拷贝出来
cp /usr/image* /mnt
再PC上查看,效果还不错
=============================
完整工程和代码:lab_v4l2_yuyv.zip
可以指定任意分辨率摄像头的代码:v4l2grab_Anysize.rar 《感谢@jiaquwang分享》