本文是在Linux操作系统下实现对USB摄像头的图像采集与显示的。由于操作系统已经有了USB摄像头的驱动,因此摄像头可以直接使用。USB摄像头的数据采集和显示分为三个步骤:USB摄像头采集数据;将采集的数据进行解码转换成RGB格式;利用Framebuffer将RGB数据显示在LCD上。
USB摄像头图像采集属于V4L2编程,可以参考Video for Linux Two API Specification这个文档。我的USB摄像头采集的数据格式是Jpeg图片,像素是320*240,下一步工作是将JPEG图片转换成RGB格式。对Framebuffer进行操作便可以显示RGB图像,即可显示摄像头采集的图像了。多祯图片连续显示便可显示连续的画面。
下面我将从后到前的顺序依次介绍这三个过程。
首先是对Framebuffer的编程实现对RGB图像的显示。
Framebuffer在硬件上对应的就是手持设备的LCD,在PC机上就是显示其了。在软件上就称为Framebuffer了,在Linux系统中,一个设备相当于一个文件,对文件的操作相当于对设备进行操作了,显示器对应的设备文件就是/dev/fb0。进行如下操作:le/dev,查看设备文件,看是否有fb0,如果有这个设备就可以进行下面的编程了。如果没有,需要修改一个文件,/boot/grub/menu.lst,在我们使用的那个系统增加如下参数,rgb = 0x317,设置为1024*768 16位色显示,然后重启便可以看到fb0了。
第一步是对fb0的初始化,读取fb0相关参数并得到内存映射地址。
int init_fb (void) {
//int fb;
struct fb_var_screeninfo fb_var; //1.open framebuffer
fb = open(\"/dev/fb0\ if (fb < 0) {
printf(\"open /dev/fb0 error!\\n\"); return -1; }
}
//2.get fb information
ioctl(fb, FBIOGET_VSCREENINFO, &fb_var); w = fb_var.xres; h = fb_var.yres;
bpp = fb_var.bits_per_pixel;
printf(\"screen information: %d * %d, bpp :%d\\n\
//3.get framebuffer address
fbmem = mmap(0, w*h*bpp/8, PROT_WRITE|PROT_READ, MAP_SHARED,fb,0);
return 0;
之前定义了全局变量,
static int fb = -1; static int w, h, bpp; static short *fbmem;
//fb0文件描述符 //显示器长度,宽度,每像素多少位
这里主要有一个函数mmap(0, w*h*bpp/8, PROT_WRITE|PROT_READ, MAP_SHARED,fb,0);
把Framebuffer映射到内存空间,长度为w*b*bpp/8个字节。然后通过对这段内存进行读取和修改,相当于对Framebuffer(硬件上就是LCD)的读取和修改。
下面是对Framebuffer的操作了,也就是对LCD进行操作了。
void fb_point (unsigned short *fbmem,int x,int y, { }
这是一个画点的函数,(x,y)代表LCD的横坐标、竖坐标,确定某一个具体fbmem[ y*w + x] = color; int w,short color)
的位置,color为颜色值。在这里我采用的是RGB565编码,通过三元色确定一个像素的颜色。R(red)占5位,G(green)占6位,B(blue)占5位,正好16位,两个字节,因此fbmem为short类型。
void display_image(unsigned short *fbmem,int w,
int imgw,int imgh,unsigned short *imgbuf)
{
int i,j; short color;
}
unsigned short *imagebuf = (unsigned short *)imgbuf;
for (j = 0 ;j < imgh;j++) { }
for (i = 0 ;i < imgw;i++) { }
color = *imagebuf;
fb_point (fbmem,10+i,10+j,w,color); imagebuf++;
这是一个显示图片的函数,参数fbmem代表mmap映射frambuffer的首地址,int w代表屏的宽度,int imgw, imgh代表图片的长和宽,imgbuf为一张图片的RGB565数据。通过此函数可以将一副图片显示在LCD上。现在唯一的问题是图片格式一般很少是RGB格式的,所以还需做的一个工作就是将其他格式的数据转换为RGB格式。下面介绍将.jpeg图片转换为RGB565的方法。
主要使用了两个函数:
u_char *decode_jpeg (char *filename, short *widthPtr, short *heightPtr); unsigned char * buf24_to16(unsigned char * buf,int w,int h);
首先是将.jpeg文件转换为rgb-8-8-8格式,返回一个队列,然后将这个队列的数据转换为rgb565格式,这两个函数会用到jpeg库的相关函数。这个图片转换的程序是亚嵌(www.akaedu.org)何老师传给我的。如果需要的话可以发邮件给我,834152646@qq.com。至此将一张.jpeg的图片显示在Lcd的工作全部完成了。
下面是最开始的一步,也是要介绍的最后一个步骤了,从摄像头获取一张Jpeg图片,也就是V4L2编程了。主要参考《Video for Linux Two API Specification》和capture.c 。由于这个相对复杂些再加上自己水平有限,我也是基于capture.c做了部分修改,所以会有些理解上可能会有些差错,请指正:834152646@qq.com
先介绍几个重要的结构体:struct v4l2_buffer, struct buffer
struct v4l2_buffer在videodev2.h可以查看的到,struct buffer是自己定义的: struct v4l2_buffer { };
struct buffer {
void * start; size_t length;
};
__u32 index;
enum v4l2_buf_type type; __u32 __u32
bytesused; flags;
field; timestamp;
enum v4l2_field struct timeval
struct v4l2_timecode timecode; __u32
sequence;
/* memory location */
enum v4l2_memory memory; union {
__u32 offset; unsigned long userptr;
} m; __u32 __u32 __u32
length; input; reserved;
首先从main函数开始看,下面是简化的一部分: int main (int argc, char ** argv) {
dev_name = \"/dev/video\";
open_device ();
init_device ();
start_capturing ();
mainloop ();
stop_capturing ();
uninit_device ();
close_device ();
exit (EXIT_SUCCESS);
return 0; }
第一步open_device()函数:
static void open_device (void) {
struct stat st;
if (-1 == stat (dev_name, &st)) {
fprintf (stderr, \"Cannot identify '%s': %d, %s\\n\ dev_name, errno, strerror (errno)); exit (EXIT_FAILURE); }
if (!S_ISCHR (st.st_mode)) {
fprintf (stderr, \"%s is no device\\n\ exit (EXIT_FAILURE); }
fd = open (dev_name, O_RDWR /* required */ | O_NONBLOCK, 0);
if (-1 == fd) {
fprintf (stderr, \"Cannot open '%s': %d, %s\\n\
dev_name, errno, strerror (errno)); exit (EXIT_FAILURE); } }
stat (dev_name, &st)使用这个函数通过设备文件名dev_name获取文件的相关属性,其中包括文件权限、文件类型、文件所有者等等一系列信息。其中比较重要的有mode_t st_mode;记录文件权限和文件类型信息。更详细的介绍可参考:
http://blog.sina.com.cn/s/blog_5c0153620100be82.html
首先判断dev_name是不是一个字符设备,是字符设备则打开这个设备文件,即打开摄像头/dev/video3,(在我的omap3530开发板上插上摄像头之后多了这个设备,所以对应video3) open设备文件后返回一个设备文件描述符fd,在以后中程序中要经常用到这个fd。
第二步是init_device( ),下面是这个函数:
static void init_device (void) {
struct v4l2_capability cap; struct v4l2_cropcap cropcap; struct v4l2_crop crop; struct v4l2_format fmt;
unsigned int min;
if (-1 == xioctl (fd, VIDIOC_QUERYCAP, &cap)) { if (EINVAL == errno) {
fprintf (stderr, \"%s is no V4L2 device\\n\ dev_name); exit (EXIT_FAILURE); } else {
errno_exit (\"VIDIOC_QUERYCAP\"); } }
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { fprintf (stderr, \"%s is no video capture device\\n\ dev_name); exit (EXIT_FAILURE); }
/* Select video input, video standard and tune here. */
}
break;
case IO_METHOD_MMAP: case IO_METHOD_USERPTR:
if (!(cap.capabilities & V4L2_CAP_STREAMING)) { }
fprintf (stderr, \"%s does not support streaming i/o\\n\
dev_name);
break; switch (io) {
case IO_METHOD_READ:
if (!(cap.capabilities & V4L2_CAP_READWRITE)) { }
fprintf (stderr, \"%s does not support read i/o\\n\
dev_name);
exit (EXIT_FAILURE);
exit (EXIT_FAILURE);
cropcap.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (0 == xioctl (fd, VIDIOC_CROPCAP, &cropcap)) { crop.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; crop.c = cropcap.defrect; /* reset to default */
if (-1 == xioctl (fd, VIDIOC_S_CROP, &crop)) { switch (errno) { case EINVAL:
/* Cropping not supported. */ break; default:
/* Errors ignored. */ break; } } } else {
CLEAR (cropcap);
/* Errors ignored. */ }
CLEAR (fmt);
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = 320; fmt.fmt.pix.height = 240;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
if (-1 == xioctl (fd, VIDIOC_S_FMT, &fmt)) errno_exit (\"VIDIOC_S_FMT\");
/* Note VIDIOC_S_FMT may change width and height. */ }
case IO_METHOD_USERPTR: }
init_userp (fmt.fmt.pix.sizeimage); break;
case IO_METHOD_MMAP:
init_mmap (); break; switch (io) {
case IO_METHOD_READ:
init_read (fmt.fmt.pix.sizeimage); break;
/* Buggy driver paranoia. */ min = fmt.fmt.pix.width * 2; if (fmt.fmt.pix.bytesperline < min)
fmt.fmt.pix.bytesperline = min;
min = fmt.fmt.pix.bytesperline * fmt.fmt.pix.height; if (fmt.fmt.pix.sizeimage < min)
fmt.fmt.pix.sizeimage = min;
xioctl (fd, VIDIOC_QUERYCAP, &cap),第一个ioctl函数用来确认这个设备是否和内核驱动相兼容,如果不兼容则返回EINVAL ,并且获取这个摄像头的相关性能参数。
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)),获取摄像头参数后判断这个设备是否是video capture device。一个switch( io )语句,在这里我们使用mmap, if (!(cap.capabilities & V4L2_CAP_STREAMING)) 判断这个设备是否支持视频流方式。
if (0 == xioctl (fd, VIDIOC_CROPCAP, &cropcap)) ,通过这个ioctl获取裁剪相关限制。首先设置v4l2_buf_type, 然后驱动会补充这个结构体的其他属性,这时可以设置其他参数,再利用 if (-1 == xioctl (fd, VIDIOC_S_CROP, &crop)),重新设置。
接着设置一帧图片的相关参数,v4l2_format相关参数设置好之后调用if (-1 == xioctl (fd, VIDIOC_S_FMT, &fmt))便可设置图片属性。
这个函数最后的一步就是init_mmap( );之前的操作都是获取摄像头的相关信息和对摄像头的设置。下面将看看init_mmap()这个函数:
static void init_mmap (void) {
CLEAR (req);
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP;
if (-1 == xioctl (fd, VIDIOC_REQBUFS, &req)) { struct v4l2_requestbuffers req;
if (EINVAL == errno) {
fprintf (stderr, \"%s does not support \" \"memory mapping\\n\ exit (EXIT_FAILURE);
} else {
errno_exit (\"VIDIOC_REQBUFS\"); } }
if (req.count < 2) {
fprintf (stderr, \"Insufficient buffer memory on %s\\n\ dev_name); exit (EXIT_FAILURE); }
buffers = calloc (req.count, sizeof (*buffers));
if (!buffers) {
fprintf (stderr, \"Out of memory\\n\"); exit (EXIT_FAILURE); }
for (n_buffers = 0; n_buffers < req.count; ++n_buffers) { struct v4l2_buffer buf;
CLEAR (buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = n_buffers;
if (-1 == xioctl (fd, VIDIOC_QUERYBUF, &buf)) errno_exit (\"VIDIOC_QUERYBUF\");
buffers[n_buffers].length = buf.length; buffers[n_buffers].start =
mmap (NULL /* start anywhere */, buf.length,
PROT_READ | PROT_WRITE /* required */,
MAP_SHARED /* recommended */, fd, buf.m.offset);
if (MAP_FAILED == buffers[n_buffers].start) errno_exit (\"mmap\"); } }
首先if (-1 == xioctl (fd, VIDIOC_REQBUFS, &req)),先设置req的count, type, memory, memory必须为V4L2_MEMORY_MMAP,count为希望申请到的buffer个数,当这个ioctl被调用时,驱动尝试分配count个buffers,并且把实际分配的个数存储到count中。手册中有这么一句话:Memory mapped buffers are located in device memory and must be allocated with this ioctl before they can be mapped into application’s address space。
if (-1 == xioctl (fd, VIDIOC_QUERYBUF, &buf)),获取struct v4l2_buffer的相关信息。
buffers[n_buffers].length = buf.length;
buffers[n_buffers].start = mmap ( buf.length,PROT_READ | PROT_WRITE , MAP_SHARED ,fd, buf.m.offset);
内存映射到摄像头的device memory,映射到内存,这样用户空间就可以读取 buffer了。
至此整个init_device( )完成了,主要做两件事,一是对摄像头参数的读取和设置,二是init_mmap( ),即将v4l2_buffer映射到用户空间以便读取。
第三步是start_capturing ();
static void start_capturing (void) {
unsigned int i;
enum v4l2_buf_type type;
switch (io) {
case IO_METHOD_MMAP:
for (i = 0; i < n_buffers; ++i) {
struct v4l2_buffer buf;
case IO_METHOD_READ:
/* Nothing to do. */ break;
CLEAR (buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i;
if (-1 == xioctl (fd, VIDIOC_QBUF, &buf))
errno_exit (\"VIDIOC_QBUF\");
}
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (-1 == xioctl (fd, VIDIOC_STREAMON, &type))
errno_exit (\"VIDIOC_STREAMON\");
break; }
这就两个ioctl,一个是VIDIOC_QBUF,v4l2_buf入列,另外一个是VIDIOC_STREAMON作用是开始captuer。
第四步是:mainloop ();
static void mainloop (void) {
unsigned int count;
count = 1000; while (count-- > 0) { for (;;) {
fd_set fds; struct timeval tv; int r;
FD_ZERO (&fds); FD_SET (fd, &fds);
/* Timeout. */ tv.tv_sec = 2; tv.tv_usec = 0;
r = select (fd + 1, &fds, NULL, NULL, &tv);
if (-1 == r) {
if (EINTR == errno) continue;
errno_exit (\"select\"); }
if (0 == r) {
fprintf (stderr, \"select timeout\\n\"); exit (EXIT_FAILURE); }
if (read_frame ())
break;
} } }
其实也就一个函数read_frame( ),这个是主要部分:
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP;
if (-1 == xioctl (fd, VIDIOC_DQBUF, &buf)) {
case IO_METHOD_MMAP:
CLEAR (buf);
switch (errno) { case EAGAIN:
}
default: }
errno_exit (\"VIDIOC_DQBUF\");
/* fall through */
case EIO:
/* Could ignore EIO, see spec. */
return 0;
assert (buf.index < n_buffers);
break;
if (-1 == xioctl (fd, VIDIOC_QBUF, &buf))
errno_exit (\"VIDIOC_QBUF\");
process_image (buffers[buf.index]);
也就三个步骤:出列process_image入列 这个是process_image();
static void process_image (struct buffer tmpbuffer) { }
将tmpbuffer的数据写到一个.jpeg文件中。至此,基本上完成所有功能了,剩下的就是后续处理了。
FILE *fp; char imagename[8];
sprintf (imagename,\"%d.jpg\fp = fopen(imagename,\"wb+\");
fwrite(tmpbuffer.start, 1, tmpbuffer.length, fp); show_jpeg(imagename); fclose(fp); imagenum++;
因篇幅问题不能全部显示,请点此查看更多更全内容