Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2279746
  • 博文数量: 668
  • 博客积分: 10016
  • 博客等级: 上将
  • 技术积分: 8588
  • 用 户 组: 普通用户
  • 注册时间: 2008-05-29 19:22
文章分类

全部博文(668)

文章存档

2011年(1)

2010年(2)

2009年(273)

2008年(392)

分类:

2009-08-01 16:04:56

作者: 陈俊宏


本期将以完整的程序范例为主, 说明之前未深入说明的地方。并且更详细地介绍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 可至 下载。

将影像资料存成 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 能力的软件。
阅读(809) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~