当前位置: 首页 > 工具软件 > Video4Linux2 > 使用案例 >

Qt/Linux 下的摄像头捕获(Video4Linux2)

璩华辉
2023-12-01

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]

 类似资料: