如何在Raspberry Pi(树莓派)上调用V4L2来操纵摄像头拍照简单地说,它就是一个基于
ARM CPU的、信用卡那么大的迷你计算机。
我曾经写过一篇教程,展示了如何调用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, suppor
ting many USB ***s, 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格式的图片。于是我看到了这行代码:
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB24;
这里显然是控制文件格式的。根据前面的链接2,我把它改成了jpg格式:
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转格式很容易。于是先安装:
pacman -S imagemagick
安装好之后,它就带了一个名为“convert”的程序,转换命令不要太简单:
convert 1.ppm 1.jpg
这样就把1.ppm转换成了1.jpg。对320*240大小的图片,我感觉转换速度是非常快的。
5、完整代码
程序文件名为 ***-stable.c :
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- /**
- * A program to capture image via the camera.
- *
- * [url=home.php?mod=space&uid=40524]@author[/url] Darran Zhang @ codelast.com
- */
- #define CLEAR(x) memset(&(x), 0, sizeof(x))
- struct buffer {
- void *start;
- size_t length;
- };
- static void xioctl(int fh, int request, void *arg)
- {
- int r;
- do {
- r = v4l2_ioctl(fh, request, arg);
- } while (r == -1 && ((errno == EINTR) || (errno == EAGAIN)));
- if (r == -1) {
- fprintf(stderr, "error %d, %sn", errno, strerror(errno));
- exit(EXIT_FAILURE);
- }
- }
- int main(int argc, char **argv) {
- if (argc < 4) {
- printf("Usage: ./*** [outputImagePath] [outputImageWidth] [outputImageHeight]nE.g. ./*** ***.jpg 320 240n");
- return 1;
- }
- char* outputImagePath = argv[1];
- int imageWidth = atoi(argv[2]);
- int imageHeight = atoi(argv[3]);
- struct v4l2_format fmt;
- struct v4l2_buffer buf;
- struct v4l2_requestbuffers req;
- enum v4l2_buf_type type;
- fd_set fds;
- struct timeval tv;
- int r, fd = -1;
- unsigned int i, n_buffers;
- char *dev_name = "/dev/video0";
- char out_name[256];
- FILE *fout;
- struct buffer *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%dn", 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");
- return errno;
- }
- 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, "P6n%d %d 255n",
- 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);
- return 0;
- }
复制代码
编译:
- gcc ***-stable.c -lv4l2 -o ***-stable
复制代码
程序运行时需要3个参数,分别是输出图片文件的路径、输出图片的宽度、输出图片的高度,例如:
- ./***-stable /home/codelast/1.ppm 320 240
复制代码
用一个脚本,在输出图片之后,再将其转为jpg格式,就完成了我们的任务。
6、总结
[1] 在解决上述问题的过程中,我遇到了无数的问题(例如编译时找不到某些库),因此为了解决那些问题,我安装了很多package,但我不确定它们是否全都需要,并且也没有来得及把它们全都记录下来,因此,我把记下来的写在这里,仅供参考:
[2] 这个程序比OpenCV版的程序要稳定得多,体现在抓取成功(程序不提示错误)的机会大得多,但我得承认,它还是有问题的,有时候会出错:
- error 12, Cannot allocate memory
复制代码
然后就没能抓下来图片。看这提示是内存不够(我是512MB的Pi)?但具体原因我还是不知道为什么,如果你知道怎么优化,请不吝赐教。