Linux下使用各种设备是一件令人兴奋的事情。在Unix的世界里,用户与硬件打交待总是简单的。最近笔者在Linux下搞了摄像头的开发,有一点感想发于此处。
Linux中操作一个设备一般都是打开(open),读取(read)和关闭(close)。使用Read的大多是一些字符型设备,然而对于显示屏 或者摄像头这种字符设备而已,挨个字的读写将使得系统调用变得频繁,众所周之,系统调用对于系统而已是个不小的开销。于是有内存映射(mmap)等物,本 例中将讲述在Linux下开发摄像头的一般过程以及使用Qt进行界面开发的实例。
使用mmap方式获取摄像头数据的方式过程一般为:
打开设备 -> 获取设备的信息 -> 请求设备的缓冲区 -> 获得缓冲区的开始地址及大小 -> 使用mmap获得进程地址空间的缓冲区起始地址 -> 读取缓冲区。
Mmap就是所谓内存映射。很多设备带有自己的数据缓冲区,或者驱动本身在内核空间中维护一片内存区域,为了让用户空间程序安全地访问,内核往往要 从设备内存或者内核空间内存复制数据到用户空间。这样一来便多了复制内存这个环节,浪费了时间。因此mmap就将目标存储区域映射到一个用户空间的一片内 存,这样用户进程访问这片内存时,内核将自动转换为访问这个目标存储区。这种转换往往是地址的线性变化而已(很多设备的存储空间在所谓外围总线地址空间 (X86)或者总的地址空间(ARM)上都是连续的),所以不必担心其转换的效率。
现在开始叙述Video4Linux2的使用。
1 /* 打开设备并进行错误检查 */ 2 3 int fd = open ("/dev/video",O_RDONLY); 4 5 if (fd==-1){ 6 7 perror ("Can't open device"); 8 9 return -1; 10 11 } 12 13 14 15 /* 查询设备的输出格式 */ 16 17 struct v4l2_format format; 18 19 memset (&format,0,sizoef(format)); 20 21 format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 22 23 if (-1==ioctl(fd,VIDIOC_G_FMT,&format)){ 24 25 perror ("While getting format"); 26 27 return -2; 28 29 } 30 31 32 33 /* 34 35 * 这里要将struct v4l2_format结构体置零,然后将 36 37 * format.type设定为V4L2_BUF_TYPE_VIDEO_CAPTURE, 38 39 * 这样在进行 VIDIOC_G_FMT 的ioctl时,驱动就会知 40 41 * 道是在捕获视频的情形下获取格式的内容。 42 43 * 成功返回后,format就含有捕获视频的尺寸大小及格 44 45 * 式。格式存储在 format.fmt.pix.pixelformat这个32 46 47 * 位的无符号整数中,共四个字节,以小头序存储。这里 48 49 * 介绍一种获取的方法。 50 51 */ 52 53 54 55 char code[5]; 56 57 unsigned int i; 58 59 for (i=0;i<4;i++) { 60 61 code[i] = (format.fmt.pix.pixelformat & (0xff<<i*8))>>i*8; 62 63 } 64 65 code[4]=0; 66 67 68 69 /* 现在的code是一个以/0结束的字符串。很多摄像头都是以格式MJPG输出视频的。 70 71 * MJPG是Motion JPEG的缩写,其实就是一些没填霍夫曼表的JPEG图片。 72 73 */ 74 75 76 77 /* 请求一定数量的缓冲区。 78 79 * 但是不一定能请求到那么多。据体还得看返回的数量 80 81 */ 82 83 struct v4l2_requestbuffers req; 84 85 memset (&req,0,sizeof(req)); 86 87 req.count = 10; 88 89 req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 90 91 req.memory = V4L2_MEMORY_MMAP; 92 93 if (-1==ioctl(fd,VIDIOC_REQBUFS,&req)){ 94 95 perror ("While requesting buffers"); 96 97 return -3; 98 99 } 100 101 if (req.count < 5){ 102 103 fprintf (stderr, "Can't get enough buffers!/n"); 104 105 return -4; 106 107 } 108 109 110 111 /* 这里请求了10块缓存区,并将其类型设为MMAP型。 */ 112 113 114 115 /* 获取缓冲区的信息 116 117 * 在操作之前,我们必须要能记录下我们 118 119 * 申请的缓存区,并在最后使用munmap释放它们 120 121 * 这里使用结构体 122 123 * struct buffer { 124 125 * void * start; 126 127 * ssize_t length; 128 129 * } 以及buffer数量 130 131 * static int nbuffer 132 133 * 来表示 134 135 */ 136 137 struct buffer * buffers = (struct buffer *)malloc (nbuffer*sizeof(*buffers)); 138 139 if (!buffers){ 140 141 perror ("Can't allocate memory for buffers!"); 142 143 return -4; 144 145 } 146 147 148 149 struct v4l2_buffer buf; 150 151 for (nbuffer=0;nbuffer<req.count;++nbuffer) { 152 153 memset (&buf,0,sizeof(buf)); 154 155 buf.type= V4L2_BUF_TYPE_VIDEO_CAPTURE; 156 157 buf.memory= V4L2_MEMORY_MMAP; 158 159 buf.index = nbuffer; 160 161 162 163 if (-1==ioctl(fd,VIDIOC_QUERYBUF,&buf)){ 164 165 perror ("While querying buffer"); 166 167 return -5; 168 169 } 170 171 172 173 buffers[nbuffer].length = buf.length; 174 175 buffers[nbuffer].start = mmap ( 176 177 NULL, 178 179 buf.length, 180 181 PROT_READ, /* 官方文档说要加上PROT_WRITE,但加上会出错 */ 182 183 MAP_SHARED, 184 185 fd, 186 187 buf.m.offset 188 189 ); 190 191 192 193 if (MAP_FAILED == buffers[nbuffer].start) { 194 195 perror ("While mapping memory"); 196 197 return -6; 198 199 } 200 201 } 202 203 204 205 /*这个循环完成后,所有缓存区都保存在 206 207 *了buffers这个数组里了,完了就再将它们munmap即可。 208 209 */ 210 211 212 213 /* 打开视频捕获 */ 214 215 enum v4l2_buf_type type; 216 217 type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 218 219 if (-1==ioctl(fd,VIDIOC_STREAMON,&type)){ 220 221 perror ("While opening stream"); 222 223 return -7; 224 225 } 226 227 228 229 /* 与内核交换缓冲区 */ 230 231 unsigned int i; 232 233 i=0; 234 235 while(1) { 236 237 memset (&buf,0,sizeof(buf)); 238 239 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 240 241 buf.memory = V4L2_MEMORY_MMAP; 242 243 buf.index = i; 244 245 246 247 if (-1==ioctl(fd,VIDIOC_DQBUF,&buf)){ 248 249 perror ("While getting buffer's data"); 250 251 return -8; 252 253 } 254 255 256 257 /* 现在就得到了一片缓冲区的数据,发送处理 */ 258 259 process_image ( buffers+buf.index,buf.index ); 260 261 262 263 /* 将缓冲区交还给内核 */ 264 265 if (-1==ioctl(fd,VIDIOC_QBUF,&buf)){ 266 267 perror ("While returning buffer's data"); 268 269 return -9; 270 271 } 272 273 274 275 i = (i+1) & nbuffer; 276 277 }
这就是所有获取的过程了。至于图像的处理,则是由解码函数和Qt来处理。现在先进行一些思路的设计。设想在进行图像的转换时,必须提供一块内存区域 来进行,我们当然可以在转换时使用malloc来进行动态分配,转换完成并显示后,再将它free。然而这样做对内核而言是一个不小的负担:每次为一整张 图片分配内存,最少也有上百KB,如此大的分配量和释放量,很容易造成内存碎片加重内核的负担。由于仅是每转换一次才显示一次图像,所以这片用于转换的内 存区域可以安全地复用,不会同时由两个线程操作之。因此在初始化时我们为每一块内存映射缓冲区分配一块内存区域作为转换用。对于MJPEG到JPEG的转 换,使用相同的内存大小。代码就不在此列出了。这片假设这个内存区域的起始指针为convertion_buffers,在process_image (struct buffer * buf, int index ) 中,有
1 void process_image (struct buffer *buf, int index){ 2 3 struct * buffer conv_buf = convertion_buffers+index; 4 5 do_image_conversion ( 6 7 buf->start, buf->length, /* 要转换的区域 */ 8 9 conv_buf->start, conv_buf->length, /* 保存转换数据的区域 */ 10 11 ); 12 13 14 15 /* 现在就可以把数据取出并交给QPixmap处理 16 17 * 要在一个QWidget里作图,必须重载paintEvent 18 19 * 函数并用QPainter作画。然而paintEvent 20 21 * 是由事件驱动层调用的,我们不能手工, 22 23 * 所以在我们自己的的重载类里要保存一个全局 24 25 * 的QPixmap。这里设为 QPixmap * m_pixmap 26 27 */ 28 29 m_pixmap -> loadFromData (conv_buf->start,conv_buf->length); 30 31 /* 立即安排一次重绘事件 */ 32 33 repaint (); 34 35 } 36 37 38 39 /* 重载的paintEvent示例 */ 40 41 MyWidget::paintEvent (QPaintEvent * evt) { 42 43 QPainter painter(this); 44 45 painter.drawPixmap (QPoint(0,0),*m_pixmap); 46 47 QWidget::paintEvent(evt); 48 49 }
这里讲Pixmap画到了(0,0)处。
考虑的改进之处:
虽然上述程序已经可以工作了,但是有一些细节可以改进。比如图像转换之处,可能相当耗时。解决的办法之一可以考虑多线程,用一个线程进行数据的收 集,每收集一帧数据便通知显示的进程。显示的进程使用一个FIFO收集数据,用一个定时器,在固定的时间到时,然后从FIFO中取出数据进行转换然后显 示。两个线程互不干扰,可以更有效地利用CPU,使收集、转换和显示协调地工作。
VN:F [1.9.6_1107]