2009年8月3日星期一

Linux的应用--Video Streaming探讨五

本期将以完整的程序范例为主,
说明之前未深入说明的地方。并且更详细地介绍video4linux 如何以 mmap
(filp-flop) 方式撷取影像资料, 同时也会展示如何将撷取出来的影像存成图档,
并且利用绘图软件开启。
mmap 的初始化从那里开始
继前四期介绍有关 Video Streaming 的内容后, 最近收到几位读者的来信,
询问有关 video4linux 利用mmap撷取影像的方法。video4linux 以 mmap
撷取影像的方法在本文第 4
篇曾经简单介绍过,但是有读者希望可以做更详细的介绍,因此笔者特别将相关的程序码完整列出供参考。
要提到 mmap 的初始化, 我们要配合第 2
篇文章的程序范例。底下是对影像撷取装置做初始化的程序码, 与第 2
篇文章的范例比较, 底下的函数设计的更完整:
int device_init(char *dev, int channel, int norm)
{
int i;
if (dev == NULL) {
dev = "/dev/video0"; //set to default device
}
if (v4l_open(dev, &vd)) {
return -1;
} else {
v4l_grab_init(&vd, screen_width, screen_height); //wake up
drivers!
v4l_close(&vd);
}
if (v4l_open(dev, &vd)) return -1;
if (v4l_get_channels(&vd)) return -1;
if (v4l_set_norm(&vd, norm)) return -1;
if (v4l_mmap_init(&vd)) return -1;
if (v4l_switch_channel(&vd, channel)) return -1;
printf("%s: initialization OK... %s\n"
"%d channels\n"
"%d audios\n\n", dev, vd.capability.name,
vd.capability.channels, vd.capability.audios);
for (i = 0; i < vd.capability.channels; i++) {
printf("Channel %d: %s (%s)\n", i, vd.channel.name,
v4l_norms[vd.channel.norm].name);
}
printf("v4l: mmap's address = %p\n", vd.map);
printf("v4l: mmap's buffer size = 0x%x\n", vd.mbuf.size);
printf("v4l: mmap's frames = %d (%d max)\n", vd.mbuf.frames,
VIDEO_MAX_FRAME);
for (i = 0; i < vd.mbuf.frames; i++) {
printf("v4l: frames %d's offset = 0x%x\n", i, vd.mbuf.offsets);
}
printf("v4l: channel switch to %d (%s)\n", channel,
vd.channel[channel].name);
// start initialize grab
if (v4l_get_picture(&vd)) return -1;
if (v4l_set_palette(&vd, DEFAULT_PALETTE)) return -1;
if (v4l_grab_init(&vd, screen_width, screen_height)) return -1;
if (v4l_grab_sync(&vd)) return -1;
return 0;
}
我们又把 device_init() 写的更完整了。粗体字的地方是我们初始化 mmap
的程序码, 一开始的程序可能又让人觉得一脸茫然:
if (v4l_open(dev, &vd)) {
return -1;
} else {
v4l_grab_init(&vd, screen_width, screen_height); //wake up drivers!
v4l_close(&vd);
}
将 device 开启成功后, 做了一次 v4l_grab_init后再把 device 关掉,
用意何在呢? 其实, 是因为bttv 的 driver 是以 module 的方式安装到
Linuxkernel, 所以 bttv driver 会因为没有被使用,而「睡觉了」。
我们加上一次 v4l_grab_init() 的目的就是为了要「叫醒」bttv 的 driver,
其实这个动作可有可无, 但一般认为加上会比较好。
v4l_mmap_init() 是对 mmap 做初始化的工作, 不过要特别注意, 这个动作要在
channel 与 norm 都设定好后才进行, 底下会再说明一次。
v4l_mmap_init() 相当重要, 因为我们要利用 mmap() 函数将 v4l_deivce 结构里的
map「连接」起来。mmap() 是 POSIX.4 的标准函数, 用途是将 device 给 map
到内存, 也就是底下粗体字的地方:
int v4l_mmap_init(v4l_device *vd)
{
if (v4l_get_mbuf(vd) < 0)
return -1;
if ((vd->map = mmap(0, vd->mbuf.size, PROT_READ|PROT_WRITE,
MAP_SHARED, vd->fd, 0)) < 0) {
perror("v4l_mmap_init:mmap");
return -1;
}
return 0;
}
PROT_READ 表示可读取该 memory page , PROT_WRITE 则是可写入,
MAP_SHARED则是让这块mapping 的区域和其它 process 分享。第一个参数旦 0
是启始位置, vd->mbuf.size则是长度(length)。vd->fd 则是 device 的 file
description, 最后一个参数是 offset。
v4l_get_mbuf() 和之前介绍过的没有什么出入。在新的 device_init() 函数里,
我们也把初始化好的 mmap 相关信息印出。
channel 与 norm
我们提过, 在做 v4l_mmap_init() 前要先做 channel 与 norm 的设定, 分别是
v4l_get_channels() 与 v4l_set_norm() 函数。
在这里要捕充说明一点, 以笔者的 CCD 头来讲, 和撷取卡是以 Composite1 连接,
所以在 channel 方面, 就要利用 v4l_switch_channel() 将 channel 切到
Composite1 端。
v4l_switch_channel() 程序码如下:
int v4l_switch_channel(v4l_device *vd, int c)
{
if (ioctl(vd->fd, VIDIOCSCHAN, &(vd->channel[c])) < 0) {
perror("v4l_switch_channel:");
return -1;
}
return 0;
}
传入的 c 是 channel, 而 channel number 我们已经在 device_init() 里打印出来:
Channel 0: Television
Channel 1: Composite1
Channel 2: S-Video
我们可以看到 Composite1 位于 Channel 1 (由 0 算起), 所以
v4l_switch_channel() 的参数 c 要传入 1。
如何设定 norm
norm 的话就比较单纯一点, 参数如下:
VIDEO_MODE_PAL
VIDEO_MODE_NTSC
VIDEO_MODE_SECAM
VIDEO_MODE_AUTO
这些参数都定义于 videodev.h 档案里。v4l_set_norm() 是我们用来设定 norm
的函数, 程序码如下:
int v4l_set_norm(v4l_device *vd, int norm)
{
int i;
for (i = 0; i < vd->capability.channels; i++) {
vd->channel.norm = norm;
}
if (v4l_get_capability(vd)) {
perror("v4l_set_norm");
return -1;
}
if (v4l_get_picture(vd)) {
perror("v4l_set_norm");
}
return 0;
}
要仔细注意, 我们是对所有的 channel 设定 norm, 设定完成后, 底下又做了一次
v4l_get_capability(), 主要目的是确保每个 channel
的设定都有被设定成功。然后呼叫 v4l_get_picture。
v4l_get_capability() 会利用 ioctl()
取得设备档的相关信息,并且将取得的信息放到structvideo_capability
结构里。同理,v4l_get_picture() 也会呼叫
ioctl(),并将影像视窗信息放到struct video_picture 结构。
如何 get picture
取得设备信息后,我们还要再取得影像信息,所谓的影像信息指的是输入到影像捕捉卡的影像格式。在
_v4l_struct 结构里,我们宣告 channel 如下:
struct video_picture picture;
初始化 picture 的意思就是要取得输入到影像捕捉卡的影像信息,我们设计
v4l_get_ picture() 函数来完成这件工作。
v4l_get_ picture () 完整程序码如下:
int v4l_get_picture(v4l_device *vd)
{
if (ioctl(vd->fd, VIDIOCGPICT, &(vd->picture)) < 0) {
perror("v4l_get_picture:");
return -1;
}
return 0;
}
传递VIDIOCGPICT 给 ioctl() 则会传回影像的属性 (image
properties),这里则是将影像属性存放于 vd->
picture。这部份我们也曾经介绍过, 在这里要再捕充一点。如果是以 GREY
方式撷取影像, 那么我们可以利用 VIDIOCSPIC 来设定像素的亮度与灰阶度, 请参考
API.html 里的 struct video_picture 说明。
初始化 grab
初始化 grab 的程序码如下:
if (v4l_get_picture(&vd)) return -1;
if (v4l_set_palette(&vd, DEFAULT_PALETTE)) return -1;
if (v4l_grab_init(&vd, screen_width, screen_height)) return -1;
if (v4l_grab_sync(&vd)) return -1;
v4l_get_picture() 与之前介绍的一样, 而 v4l_set_palette()
则是用来设定调色盘, 由于我们希望得到的是 RGB32, 所以 DEFAULT_PALETTE
定义成:
#define DEFAULT_PALETTE VIDEO_PALETTE_RGB32
如果没有硬件转换, 前一篇文章 (4) 我们也提到将 YUV (PAL) 转成 RGB
的方法了。再来将就是对 grab 做初始化, v4l_grab_init()
int v4l_grab_init(v4l_device *vd, int width, int height)
{
vd->mmap.width = width;
vd->mmap.height = height;
vd->mmap.format = vd->picture.palette;
vd->frame_current = 0;
vd->frame_using[0] = FALSE;
vd->frame_using[1] = FALSE;
return v4l_grab_frame(vd, 0);
}
初始化的目的是将 mmap 结构填入适当的值。针对 RGB32、NTSC 的 CCD 影像撷取,
mmap 的大小不妨设定成 640*480 或 320*240 都可以, 给定 mmap 的大小后,
再来还要将 format 填入调色盘类型。
最后设定 frame_current 变数与 frame_using[] 数组, 这里等于上一篇 (4)
介绍的 frame 变数与 framestat[] 数组。如何所有的程序码都没有错误,
当装置正常躯动时, 就可以看到底下的初始化讯息,
这里的讯息比起之前的范例更清楚、完整:
/dev/video0: initialization OK... BT878(Chronos Video Shuttle I)
3 channels
3 audios
Channel 0: Television (NTSC)
Channel 1: Composite1 (NTSC)
Channel 2: S-Video (NTSC)
v4l: mmap's address = 0x40173000
v4l: mmap's buffer size = 0x410000
v4l: mmap's frames = 2 (32 max)
v4l: frames 0's offset = 0x0
v4l: frames 1's offset = 0x208000
v4l: channel switch to 1 (Composite1)
Image pointer: 0x4037b000
v4l_grab_frame() 的用处
读者可能还不明白 v4l_grab_frame() 的用途, v4l_grab_frame()
是真正将影像放到 mmap 里的函数。我们重写一次 v4l_grab_frame() 函数,
并且再说明一次:
int v4l_grab_frame(v4l_device *vd, int frame)
{
if (vd->frame_using[frame]) {
fprintf(stderr, "v4l_grab_frame: frame %d is already
used.\n", frame);
return -1;
}
vd->mmap.frame = frame;
if (ioctl(vd->fd, VIDIOCMCAPTURE, &(vd->mmap)) < 0) {
perror("v4l_grab_frame");
return -1;
}
vd->frame_using[frame] = TRUE;
vd->frame_current = frame;
return 0;
}
因为我们用 frame_using[] 数组来纪录那个 frame 已经被使用,
所以一开始当然要先判断目前的 frame 是否已经被使用:
if (vd->frame_using[frame]) {
fprintf(stderr, "v4l_grab_frame: frame %d is already used.\n",
frame);
return -1;
}
如果没有被使用, 就把 mmap 的 frame 填入 frame 编号, 然后利用
VIDIOCMCAPTURE撷取出影像。结束前要把目前frame 的状态标示成使用中
(frame_using[]), 然后把 frame_current 指定成现在的frame, 完成工作后离开。
mmap 如何做 filp-flop
这是一位读者问的问题。这个问题问的相当聪明, 每个人可能都有不同的方法来做
flip-flop 的动作, 这里笔者以 2 个 frame 为例, 我们可以再写一个函数来做
flip-flop:
int device_grab_frame()
{
vd.frame_current = 0;
if (v4l_grab_frame(&vd, 0) < 0)
return -1;
return 0;
}
int device_next_frame()
{
vd.frame_current ^= 1;
if (v4l_grab_frame(&vd, vd.frame_current) < 0)
return -1;
return 0;
}
device_next_frame() 是主要核心所在, 因为我们只有二个 frame, 所以
frame_current 不是 0 就是 1。
撷取出来的影像放在那里
因为我们特别写了上面的函数来做 mmap 的 flip-flop, 所以在主程序里就改用
device_next_frame 来持续撷取影像。所以配合主程序, 我们的程序写法如下:
device_next_frame(); //Ok, grab a frame.
device_grab_sync(); //Wait until captured.
img = device_get_address(); //Get image pointer.
printf("\nImage pointer: %p\n", img);
这段程序就是我们的重点好戏, 当我们呼叫 device_next_frame() 撷取 frame
之后, 必须做一个等待的动作, 让 frame 撷取完成再取出影像。
v4l_grab_sync() 程序码如下:
int v4l_grab_sync(v4l_device *vd)
{
if (ioctl(vd->fd, VIDIOCSYNC, &(vd->frame_current)) < 0) {
perror("v4l_grab_sync");
}
vd->frame_using[vd->frame_current] = FALSE;
return 0;
}
利用 VIDIOCSSYNC 等待完成后, 别忘了将目前 frame
的状态改回未被使用。接下来我们要问, 撷出出来的 frame 到底放到那里去了呢?
答案就是之们利用 mmap() 将 device 所 map 的内存里, 因为我们是利用 mmap
(flip-flop) 方式, 所以会有 2 个 (或以上) 的 frame, 这时就要计算一下
offset, 才知道到底目前的影像资料被放到那里了。算式如下:
vd.map + vd.mbuf.offsets[vd.frame_current]
device_get_address() 函数就是这么回事。
如何输出影像资料呢
输出影像资料的方法很多, 可以直接输出到 framebuffer 上, 或是利用 SDL
显示。在这里笔者要示范最原始的方法 ━ 输出到档案里。当我们利用
device_get_address() 取得 frame 的影像资料后, 再将 frame 的影像资料输出成
PPM 格式的档案。程序码如下:
FILE *fp;
fp = fopen("test.ppm", "w");
fprintf(fp, "P6\n%d %d\n255\n", NTSC_WIDTH, NTSC_HEIGHT);
fwrite(img, NTSC_WIDTH, 3*NTSC_HEIGHT, fp);
fclose(fp);
先利用 fprintf() 写入 PPM 档案的档头信息, 然后以 fwrite()
将传回的影像资料写到档案里。img 指向内存里的 frame 影像资料, 写入时,
请特别注意粗体字的地方, 因为我们是用 RGB32 的调色盘, 而 RGB 是以 3 个
sample 来表示一个 pixel, 所以要乘上 3。如果是 GREY 调色盘, 就不用再乘 3
了。最后将输出的 PPM 档案转换格式成 TIFF 就可以用一盘的绘图软件打开了:
linux$ ppm2tiff test.ppm test.tiff
将影像存成 JPEG 的方法
最后我们再完成一个功能, 就可以实作出一个完整的 Webcam
软件。之前我们将影像存成 PPM 格式的图档, 不过因为档案过太,
会造成传输的不便。因此, 我们势必要将影像资料存成更小的档案才具实用性。JPEG
或MJPEG 都是在本文第 1 篇介绍过的格式。以 JPEG 来存放图档,
相当容易可以实作出 Webcam 的功能, 但缺点就是无法传送声音资料。
我们使用 mpeglib 来完成这项任务, mpeglib 可至 www.ijg.org 下载。
将影像资料存成 JPEG 的方法在「各大」与 video streaming 有关的软件 (例如:
xawtv) 都可以看得到范例。不过因此这部份已脱离 v4l 的主,
所以笔者只列出底下的 write_jpeg() 完整函数, 供读者使用:
int write_jpeg(char *filename, unsigned char * img, int width, int
height, int quality, int gray)
{
struct jpeg_compress_struct jcfg;
struct jpeg_error_mgr jerr;
FILE *fp;
unsigned char *line;
int line_length;
int i;
if ((fp = fopen(filename,"w")) == NULL) {
fprintf(stderr,"write_jpeg: can't open %s: %s\n", filename,
strerror(errno));
return -1;
}
jcfg.image_width = width;
jcfg.image_height = height;
jcfg.input_components = gray ? 1: 3; // 3 sample per pixel
(RGB)
jcfg.in_color_space = gray ? JCS_GRAYSCALE: JCS_RGB;
jcfg.err = jpeg_std_error(&jerr);
jpeg_create_compress(&jcfg);
jpeg_stdio_dest(&jcfg, fp);
jpeg_set_defaults(&jcfg);
jpeg_set_quality(&jcfg, quality, TRUE);
jpeg_start_compress(&jcfg, TRUE);
line_length = gray ? width : width * 3;
for (i = 0, line = img; i < height; i++, line += line_length)
jpeg_write_scanlines(&jcfg, &line, 1);
jpeg_finish_compress(&jcfg);
jpeg_destroy_compress(&jcfg);
fclose(fp);
return 0;
}
利用 mpeglib 写入 JPEG 影像资料时, 必须分别对每行 scanline 写入。呼叫范例:
write_jpeg("test01.jpg", img, NTSC_WIDTH, NTSC_HEIGHT, 50, FALSE );
第一个参数是图档名称, 第二个参数是影像资料,
然后第三、第四个参数接着影像的大小, 第五个参数 50 表示 JPEG 图档的压缩品质
(quality), 最后一个参数 FALSE 表示影像资料不是 grey (灰阶)
影像。灰阶影像与彩色影像的差别在于 input_components、in_color_space 与
scanline 的长度。
结语
在一连串的 Video Streaming 主题里, 我们学到 video4linux 撷取影像的方式, 以
mmap(flip-flop)来连续撷取影像, 并做到 VOD
的功能是我们的最终目的。到这里为止, 我们已经有能力实作出简单的
Webcam软件,类似这种取固定间隔传送影像的方式应用也很广,
例如路口交通状况回报。
利用到这里所学的方法, 将撷取的影像存成 JPEG, 然后放到 Web 上,
固定一段时间更新, 我们也可以设计一套简单的路口交通状况回报系统,
或是家里的监视系统。后面接着的主题, 将会以现有的程序为基础, 实作真正具有
VOD 能力的软件。

没有评论: