Linux的應用--Video Streaming探討 五

作者: 陳俊宏
www.jollen.org

本期將以完整的程序範例爲主, 說明之前未深入說明的地方。並且更詳細地介紹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 能力的軟件。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章