Linux的應用–Video Streaming探討五

來源:互聯網
上載者:User
本期將以完整的程式範例為主,
說明之前未深入說明的地方。並且更詳細地介紹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 能力的軟體。
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.