Email me: jollen # jollen.org

more: Jollen 的 Embedded Linux 教育訓練

« August 2001 | (回到Blog入口) | October 2001 »

September 2001 歸檔

September 14, 2001

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 能力的軟體。

關於 September 2001

此頁面包含了在September 2001發表於Jollen's Blog的所有日記,它們從老到新列出。

前一個存檔 August 2001

後一個存檔 October 2001

更多信息可在 主索引 頁和 歸檔 頁看到。

Top | 授權條款 | Jollen's Forum: Blog 評論、討論與搜尋
Copyright(c) 2006 www.jollen.org