我曾经写过一篇教程,展示了如何调用OpenCV,来控制摄像头拍照。在那篇文章的末尾,我提到了一个遗留问题,即把抓取图像的尺寸设置得稍高一点的时候,那个程序就很容易崩溃;在图像尺寸设置得很低的时候,有时候也会崩溃,我一直没弄明白为什么。
但是在前段时间,由于参赛原因,被迫在很短的时间内解决这个棘手的问题。真可谓是搜了无数网页,最后采取了一种折衷的方式“在很大程度上”解决了这个问题——毕竟还不完美。
所以在这篇文章里,我打算介绍另一种方式来实现控制摄像头拍照,并且程序稳定得多(尽管程序还是会崩溃,但比OpenCV的版本少多了)。
『1』把OpenCV换掉,选择知多少
当时我第一个想法就是把OpenCV换掉,但是我还有什么选择呢?我先找到了这个网页,它介绍了一种使用Python抓取摄像头图像的方法。但有个问题:如果你没有安装他们移植的系统(我应该没理解错吧...),那么就只能先按照要求安装他们提供的一些自定义library。很不巧,我使用的是Arch Linux ARM,所以当然要安装。不过,当我打开安装说明页之后,照做了几步之后,我就放弃了,因为太麻烦,路径都还要自己根据情况修改的,等我完全搞好之后,估计一个小时都过去了,而且还不一定能成。
于是我又找到了若干篇用V4L来实现图像抓取的文章(链接1,链接2),于是我抄来代码,在几乎看不懂的情况下,硬着头皮尝试去小修小改。
『2』V4L2是什么
从前面提供的链接里,大家可以看到“V4L2 video picture grabber”的说明。V4L2是什么?从 这个Wiki页面 的部分内容:
Video4Linux or V4L is a video capture[1] and output device API and driver framework for the Linux kernel, supporting many USB webcams, TV tuners, and other devices. Video4Linux is closely integrated with the Linux kernel. V4L2 is the second version of V4L. Video4Linux2 fixed some design bugs and started appearing in the 2.5.X kernels.我们知道了V4L2是Linux内核的视频抓取和输出设备API和驱动框架,它支持非常多的USB摄像头等设备。所以,用它可以驱动摄像头抓取图像。
『3』输出什么格式的图片
显然,我们想要摄像头抓取并输出jpg,png之类的图片,但是,前面链接1里的代码输出的是PPM格式的图片。于是我看到了这行代码:
1 | fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB24; |
这里显然是控制文件格式的。根据前面的链接2,我把它改成了jpg格式:
1 2 3 4 5 6 | fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_JPEG; // ... if(fmt.fmt.pix.pixelformat != V4L2_PIX_FMT_JPEG) { printf("Libv4l didn't accept jpg format. Can't proceed.\n"); exit(EXIT_FAILURE); } |
可惜的是编译运行之后发现这里过不去,也就是不支持jpg格式。于是只能输出PPM格式的图片。PPM是“可移植像素图格式”的简称,它是没有压缩的,文件体积较大(一个640*480的PPM图片竟然有900多K),所以它既不利于存储也不利于网络传输。
『4』PPM图片转换为jpg格式
前面我说过,最终得到摄像头抓取图片的方式是一种折衷的方式,这里就是一个体现:先拿到PPM,再转为jpg,实在是不得已而为之,因为通过调用V4L2抓取图像比较稳定,而我又无法让抓取图像的程序直接输出jpg格式的图片,因此就再转一下。
找来找去发现了用ImageMagick转格式很容易。于是先安装:
1 | pacman -S imagemagick |
安装好之后,它就带了一个名为“convert”的程序,转换命令不要太简单:
1 | convert 1.ppm 1.jpg |
这样就把1.ppm转换成了1.jpg。对320*240大小的图片,我感觉转换速度是非常快的。
『5』完整代码
程序文件名为 webcam-stable.c :
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | #include #include #include #include #include #include #include #include #include #include #include #include /** * A program to capture image via the camera. * * @author Darran Zhang @ codelast.com */ #define CLEAR(x) memset(&(x), 0, sizeof(x)) structbuffer { void *start; size_tlength; }; staticvoidxioctl(intfh,intrequest,void*arg) { intr; do{ r = v4l2_ioctl(fh, request, arg); }while(r == -1 && ((errno== EINTR) || (errno== EAGAIN))); if(r == -1) { fprintf(stderr,"error %d, %s\n",errno,strerror(errno)); exit(EXIT_FAILURE); } } intmain(intargc,char**argv) { if(argc < 4) { printf("Usage: ./webcam [outputImagePath] [outputImageWidth] [outputImageHeight]\nE.g. ./webcam webcam.jpg 320 240\n"); return1; } char* outputImagePath = argv[1]; intimageWidth =atoi(argv[2]); intimageHeight =atoi(argv[3]); structv4l2_format fmt; structv4l2_buffer buf; structv4l2_requestbuffers req; enumv4l2_buf_type type; fd_set fds; structtimeval tv; int r, fd = -1; unsignedint i, n_buffers; char *dev_name ="/dev/video0"; char out_name[256]; FILE *fout; structbuffer *buffers; fd = v4l2_open(dev_name, O_RDWR | O_NONBLOCK, 0); if(fd < 0) { perror("Cannot open device"); exit(EXIT_FAILURE); } CLEAR(fmt); fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = imageWidth; fmt.fmt.pix.height = imageHeight; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB24; //fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_JPEG; fmt.fmt.pix.field = V4L2_FIELD_INTERLACED; xioctl(fd, VIDIOC_S_FMT, &fmt); if(fmt.fmt.pix.pixelformat != V4L2_PIX_FMT_RGB24) { printf("Libv4l didn't accept RGB24 format. Can't proceed.\n"); exit(EXIT_FAILURE); } if((fmt.fmt.pix.width != imageWidth) || (fmt.fmt.pix.height != imageHeight)) printf("Warning: driver is sending image at %dx%d\n", fmt.fmt.pix.width, fmt.fmt.pix.height); CLEAR(req); req.count = 2; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; xioctl(fd, VIDIOC_REQBUFS, &req); buffers =calloc(req.count,sizeof(*buffers)); for(n_buffers = 0; n_buffers < req.count; ++n_buffers) { CLEAR(buf); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = n_buffers; xioctl(fd, VIDIOC_QUERYBUF, &buf); buffers[n_buffers].length = buf.length; buffers[n_buffers].start = v4l2_mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); if(MAP_FAILED == buffers[n_buffers].start) { perror("mmap"); exit(EXIT_FAILURE); } } for(i = 0; i < n_buffers; ++i) { CLEAR(buf); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; xioctl(fd, VIDIOC_QBUF, &buf); } type = V4L2_BUF_TYPE_VIDEO_CAPTURE; xioctl(fd, VIDIOC_STREAMON, &type); do{ FD_ZERO(&fds); FD_SET(fd, &fds); /* Timeout. */ tv.tv_sec = 2; tv.tv_usec = 0; r = select(fd + 1, &fds, NULL, NULL, &tv); }while((r == -1 && (errno= EINTR))); if(r == -1) { perror("select"); returnerrno; } CLEAR(buf); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; xioctl(fd, VIDIOC_DQBUF, &buf); fout =fopen(outputImagePath,"w"); if(!fout) { perror("Cannot open image"); exit(EXIT_FAILURE); } fprintf(fout,"P6\n%d %d 255\n", fmt.fmt.pix.width, fmt.fmt.pix.height); fwrite(buffers[buf.index].start, buf.bytesused, 1, fout); fclose(fout); xioctl(fd, VIDIOC_QBUF, &buf); type = V4L2_BUF_TYPE_VIDEO_CAPTURE; xioctl(fd, VIDIOC_STREAMOFF, &type); for(i = 0; i < n_buffers; ++i) { v4l2_munmap(buffers[i].start, buffers[i].length); } v4l2_close(fd); return0; } |
编译:
1 | gcc webcam-stable.c -lv4l2 -o webcam-stable |
程序运行时需要3个参数,分别是输出图片文件的路径、输出图片的宽度、输出图片的高度,例如:
1 | ./webcam-stable /home/codelast/1.ppm 320 240 |
用一个脚本,在输出图片之后,再将其转为jpg格式,就完成了我们的任务。
『6』总结
[1] 在解决上述问题的过程中,我遇到了无数的问题(例如编译时找不到某些库),因此为了解决那些问题,我安装了很多package,但我不确定它们是否全都需要,并且也没有来得及把它们全都记录下来,因此,我把记下来的写在这里,仅供参考:
1 | pacman -S xf86-video-v4l |
[2] 这个程序比OpenCV版的程序要稳定得多,体现在抓取成功(程序不提示错误)的机会大得多,但我得承认,它还是有问题的,有时候会出错:
error 12, Cannot allocate memory
然后就没能抓下来图片。看这提示是内存不够(我是512MB的Pi)?但具体原因我还是不知道为什么,如果你知道怎么优化,请不吝赐教。