Linux 的应用(刊载于PC2000 杂志十一月号)-- Video Streaming 探讨(5)
本期将以完整的程式范例为主, 说明之前未深入说明的地方。并且更详细地介绍video4linux 如何以mmap (filp-flop) 方式撷取影像资料, 同时也会展示如何将撷取出来的影像存成图档, 并且利用绘图软体开启。
作者:陈俊宏www.jollen.org
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[i].name,v4l_norms[vd.channel[i].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[i]); } 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 的方式安装到Linux kernel, 所以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[i].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() 取得设备档的相关资讯,并且将取得的资讯放到struct video_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 能力的软体。
--jollen 原文地址 http://www.jollen.org/blog/2001/09/linux_video_streaming_5.html