2009年12月7日 星期一

ffmpeg 中 swscale 的用法

如果想將某個PixelFormat轉換至另一個PixelFormat,例如,將YUV420P轉換成YUYV422,或是想變換圖的大小,都可以使用swscale達成。

其中,PixelFormat 的列表在 libavutil/pixfmt.h 內定義。

swscale的用法可以參考libswscale/swscale-example.c的sample code。主要function有三個

  • sws_getContext()
  • sws_scale()
  • sws_freeContext()

其中,我們可以把sws_getContext() 看成初始化函數,把sws_freeContext()看成結束函數。這兩個函數分別在起始及結束各執行一次即可。

真正主要的函數,是sws_scale()。

sws_getContext() 的宣告如下

SwsContext *sws_getContext(int srcW, int srcH, enum PixelFormat srcFormat, int dstW, int dstH, enum PixelFormat dstFormat, int flags, SwsFilter *srcFilter, SwsFilter *dstFilter, const double *param)

總共有十個參數,其中,較重要的是前七個;
前三個參數分別代表 source 的寬、高及PixelFormat;
四到六個參數分別代表 destination 的寬、高及PixelFormat;
第七個參數則代表要使用哪種scale的方法;此參數可用的方法可在 libswscale/swscale.h 內找到。

最後三個參數,如無使用,可以都填上NULL。

sws_getContext會回傳一個 SwsContext struct,我們可以把這個 struct 看成是個 handler,之後的sws_scale和sws_freeContext皆會用到。

以下是一個sws_getContext的簡單例子:

struct SwsContext *img_convert_ctx;
img_convert_ctx = sws_getContext(in_width, in_height, PIX_FMT_YUV420P,
out_width, out_height, PIX_FMT_YUV420P, SWS_POINT,
NULL, NULL, NULL);

一開始,我們宣告img_convert_ctx 為指向 SwsContext 的一個 pointer;接著,我們將 sws_getContext 的回傳值賦予給img_convert_ctx。

注意sws_getContext的參數;in_width及in_height分別代表 source 的寬及高,out_width及out_height分別代表轉換後的寬與高;input 和 output 的 PixelFormat 皆為 YUV420P;使用SWS_POINT的scale方法。

初始化完成後,接著就要進行主要的 scale 動作;我們透過 sws_scale() 完成。sws_scale() 的宣告如下

int sws_scale(SwsContext *c, uint8_t* src[], int srcStride[], int srcSliceY, int srcSliceH, uint8_t* dst[], int dstStride[])

總共有七個參數;
第一個參數即是由 sws_getContext 所取得的參數。
第二個 src 及第六個 dst 分別指向input 和 output 的 buffer。
第三個 srcStride 及第七個 dstStride 分別指向 input 及 output 的 stride;如果不知道什麼是 stride,姑且可以先把它看成是每一列的 byte 數。
第四個 srcSliceY,就註解的意思來看,是指第一列要處理的位置;這裡我是從頭處理,所以直接填0。想知道更詳細說明的人,可以參考 swscale.h 的註解。
第五個srcSliceH指的是 source slice 的高度。

舉一個例子如下

sws_scale(img_convert_ctx, inbuf, inlinesize, 0, in_height, outbuf, outlinesize);

這裡應該比較好懂,可以參考上面的參數說明。

最後,全部處理完後,需呼叫sws_freeContext() 結束。用法很簡單,把sws_getContext 取得的參數填入即可。如下

sws_freeContext(img_convert_ctx);

最後再整理一次,要使用swscale,只要使用 sws_getContext() 進行初始化、sws_scale() 進行主要轉換、sws_freeContext() 結束,即可完成全部動作。

以下為一個簡單的範例程式,可從foreman.yuv內取出第一張圖,轉換大小後存成另一張圖。

=====================================================================================

/*
* 需設定 SRCFILE 及 DSTFILE, 長寬等資訊
* 需 link libswscale
* 主要有三個 function
* sws_getContext() 是 initial 用, sws_freeContext() 是結束用
* sws_scale() 是主要運作的 function
* 預設只會轉換第一張 YUV, 如果要轉換整個檔, 可以把 Decoding loop 的註解拿掉
*/

#include "libswscale/swscale.h"

#define SRCFILE "foreman_cif.yuv"
#define DSTFILE "out.yuv"

int main()
{
// 設定原始 YUV 的長寬
const int in_width = 352;
const int in_height = 288;
// 設定目的 YUV 的長寬
const int out_width = 640;
const int out_height = 480;

const int read_size = in_width * in_height * 3 / 2;
const int write_size = out_width * out_height * 3 / 2;
struct SwsContext *img_convert_ctx;
uint8_t *inbuf[4];
uint8_t *outbuf[4];
int inlinesize[4] = {in_width, in_width/2, in_width/2, 0};
int outlinesize[4] = {out_width, out_width/2, out_width/2, 0};

uint8_t in[352*288*3>>1];
uint8_t out[640*480*3>>1];

FILE *fin = fopen(SRCFILE, "rb");
FILE *fout = fopen(DSTFILE, "wb");

if(fin == NULL) {
printf("open input file %s error.\n", SRCFILE);
return -1;
}

if(fout == NULL) {
printf("open output file %s error.\n", DSTFILE);
return -1;
}

inbuf[0] = malloc(in_width*in_height);
inbuf[1] = malloc(in_width*in_height>>2);
inbuf[2] = malloc(in_width*in_height>>2);
inbuf[3] = NULL;

outbuf[0] = malloc(out_width*out_height);
outbuf[1] = malloc(out_width*out_height>>2);
outbuf[2] = malloc(out_width*out_height>>2);
outbuf[3] = NULL;

// ********* Initialize software scaling *********
// ********* sws_getContext **********************
img_convert_ctx = sws_getContext(in_width, in_height, PIX_FMT_YUV420P,
out_width, out_height, PIX_FMT_YUV420P, SWS_POINT,
NULL, NULL, NULL);
if(img_convert_ctx == NULL) {
fprintf(stderr, "Cannot initialize the conversion context!\n");
return -1;
}

fread(in, 1, read_size, fin);

memcpy(inbuf[0], in, in_width*in_height);
memcpy(inbuf[1], in+in_width*in_height, in_width*in_height>>2);
memcpy(inbuf[2], in+(in_width*in_height*5>>2), in_width*in_height>>2);

// ********* 主要的 function ******
// ********* sws_scale ************
sws_scale(img_convert_ctx, inbuf, inlinesize,
0, in_height, outbuf, outlinesize);

memcpy(out, outbuf[0], out_width*out_height);
memcpy(out+out_width*out_height, outbuf[1], out_width*out_height>>2);
memcpy(out+(out_width*out_height*5>>2), outbuf[2], out_width*out_height>>2);

fwrite(out, 1, write_size, fout);

// ********* 結束的 function *******
// ********* sws_freeContext *******
sws_freeContext(img_convert_ctx);

fclose(fin);
fclose(fout);

return 0;
}

=====================================================================================

以下兩張圖為執行結果

Input Image

foreman_352x288

Output Image

foreman_640x480

2009年8月22日 星期六

Qt Debug 方法

最近為了好幾件 project 忙的焦頭爛額,其中一個案子,是負責GUI及整合各組件,其中GUI的部份,是以Qt實作。

當然,這麼重的工作量不會是一個人完成,不過也沒好到哪裡去,兩個人。

和另一個同事co-work了一個星期。project leader一直希望我們分工,我則是以兩個人都是Qt新手,一起co-work,彼此發現盲點,會比較快上手的理由,兩個人坐在同一張桌子,隨時coding,隨時討論,連續工作了一個禮拜。

這個星期工作的進度還不少,整個UI的架構已經有個雛型出來了,而且也漸漸上手了。不過星期五又被project leader壓schedule,本來是拉到十月中的schedule,一句如果兩個人分工,時間可以縮短一半的說法,聽到我下巴差點掉下來。

這星期的工作,發現了一件事:有些地方,用了很腦殘的寫法,另一個同事偶爾會發現,五分鐘指出來,當場修正;如果一個人枯坐在那邊找,可能找了兩個小時也找不出來。五分鐘和兩個小時,縮短一半的時間,嗯……

怎麼會扯這麼多,本來是要寫Qt簡易的debug方法而已 = =

星期五早上,一到辦公室,很快樂地把前一天晚上的程式碼 copy 到另一個目錄,很快樂地執行,死掉!跑回原來的目錄,正常執行。檢查程式內的圖檔是否有使用絕對路徑,檢查了dll檔有沒有在正確的目錄,檢查了各個object有沒有去指到Null pointer,檢查了所有想得到的點,還是一樣。

後來請jarsing來看,make release的可以執行,make debug的不能執行。好吧,將就一點,以後就用 make release。

到了下午,連make release都不行了。問題大了,也不知道怎麼去debug。

在網路上搜尋了一下子,發現有個很簡單的作法:原來Qt也可以用 gdb 去debug,而且安裝完成後,裡面就有gdb了。

進gdb,run,backtrace,找到了錯誤的點,很腦殘,一個為了測試用的button,設定為按↑鍵後,會跳到另一個object,可是那個object還沒實體化。就這樣,看運氣,運氣不好踩到錯誤的記憶體空間,就當了。

可是,在windows下怎麼做傳統C的printf動作?還是沒解。

假日無聊,想起jarsing有寫過一系列Qt的文章,跑上去看,果然有寫,使用qDebug(),然後在pro裡面加上CONFIG = console就行了。

後來,洗澡的時候,想到這件事,一直覺得應該有個方法可以在compile時決定qDebug()要不要把訊息印出來,這樣,在release的時候,就可以把debug message關掉。

晚上找了一下qt的官方文件,在 http://qt.nokia.com/doc/4.5/debug.html 有說明 debug 的方法,其中提到,只要define QT_NO_DEBUG_OUTPUT 這個 symbol,就可以不讓qDebug()的訊息印出來;另外,QT_NO_WARNING_OUTPUT則可以不讓qWarning()印出來。

報告完畢!玩了一個星期,發現,GUI還真的有點意思。

 

最後,誰是 jarsing?他是我的一個好朋友,在 google 上找找這個人就知道他是何方神聖了。

2009年6月3日 星期三

三庫連走。活著真好

時間:2009/06/01

端午節連假結束的第一天,請假。

上次騎車是什麼時候?如果扣掉一兩次的寶二練習,應該是年初的峨眉湖吧。很久很久以前的事了。

原本預計要騎苗栗仙山,一起床就很墮落地打了一整個早上的電動。下午臨時改成水庫連騎。

本日心得:對水庫要存敬畏之心!不要看到水庫就想環它,不是每個水庫都像寶二,繞一圈輕輕鬆鬆十公里就打發了。運氣不好沒頭沒腦看到石門水庫就要去環,還只準備一小時的時間,在羅馬公路上是會哭出來的。

幸好,永和山水庫不是石門水庫。

早上大致規劃了一下路線,中興路轉學府路上寶山水庫,走竹40接竹43,到峨眉湖,然後接台三線,到三灣時接苗17-3,然後接苗5繞永和山水庫一圈,接回台3;回程時走苗20,接苗20-2,接竹41,從峨眉出來接台3,到達北埔時再上寶二,然後很快樂地回家。

事。情。不。會。這。麼。順。利。的!

到峨眉湖的路上次走過,不會迷路。但是一到峨眉湖後就開始認路之旅。原本預計是竹43接竹81接苗竹18,然後接台3,結果查地圖時,一時疏忽,沒注意到竹81,所以竹43就一直走下去接台3,大埔水庫只走到一點點就從台3線冒出來了。

 

IMG_1267 路邊某間廢棄廠房,好久不見的標語

 

到了三灣後,原本一直在找苗17-3,看到有老街的路標,直接右轉進去,走了一段後,看到三灣國小的… 不知道是不是校門口;旁邊有一間五穀廟,再往前的一個交叉路口,看到永和山水庫往右的路標,可是沒看到苗17-3,姑且相信路標,右轉後看到一座橋,現在應該是枯水期,橋下的溪水只有一點點,河床大部份都是小石頭。原本以為這就是永和山水庫,預計往前騎個十分鐘就可以回頭了。

十分鐘?太天真了!

這條只是中港溪,要看到永和山水庫,還得上上下下幾段山路。

 

IMG_1269 三灣老街… 看起來好像就是一般的小街道 …

 

無知是幸福的,當時所能做的,只是埋著頭一直往前騎,中間有個地方在做拓寬工程,但不影響單車行進。

到達一個叉路口,上面的路標有點複雜,在這裡停了一下,判斷應該是繼續沿著苗17-3走沒錯。

再往前走五分鐘左右,看到了遠方的水庫現身了,確定路線沒有走錯。路邊正好有個地圖,靠過去一看,咦?這個路線怎麼沒有環湖? @@ 先照起來,說不定待會兒派得上用場。

 

IMG_1298 導覽地圖,不過沒有環完整個湖

 

接著又騎了六七分鐘,就看到永和山水庫的標誌了。往下看,可以看到水庫的大壩。仔細看,大壩上有很多很多的「水」字。

IMG_1308

接著就是一大段下坡,兩分鐘後就可以看到永和山水庫的大門了。

接著,如之前所規劃的,要找苗5了。騎著騎著,咦?東興橋?啥?過頭了過頭了。趕緊把剛剛路邊拍下來的地圖打開來看,回頭以後,要找到福德祠,然後右轉。瞪大眼睛一直在找福德祠,在右手邊找到了,小小的。再往前走一段,在一家OK旁找到了路,不過,那個路標被徹底遮住了,很難發現苗5就是這條。

右轉後,就開始折磨人的上上下下之旅,在此可以深刻瞭解到,為什麼之前那張導覽圖沒有這條看起來似乎是環水庫的道路;回來看地圖後才發現,去程6公里,回程變成10公里,而且一直都在山裡轉,從OK上山一直到下山,時間大約半小時多一點而已,但不確定有沒有走對路及天黑時間一直逼近的壓力,感覺時間似乎過了很久。

 

IMG_1324 苗五山腳山下的草屋

 

在老銃櫃登山步道旁,停下來照相時,雙腳的膝蓋出現抽筋的前兆,趕緊坐下來休息一下,幸好沒有抽起來。

自此,一直到回到目的地,雙腳都不敢太用力,深怕真的抽起來,我還在苗栗啊,要怎麼回三十公里外的新竹。

接到台3線後,在一家7-11旁看到苗20的路標,騎進去一段路後,看到苗20-1的路標,右轉後又騎了一段路,沿路都沒再看到路標,此時膝蓋又開始隱約要抽筋,停下來休息一下,慢慢牽了一小段上坡,再往前騎一小段,看到前方不遠有個大上坡,馬上決定更改行程,回頭沿著熟悉的台3線回家。

這應該是今天所做最正確的決定了;我走的那條路根本就是錯的!

地圖上畫的是苗20-2,我記成苗20-1,照地圖上所畫,我最後會騎到沒路,不會接到竹41去。

況且,在那個時間點和膝蓋的狀況,就算我真的找對路,也不一定能在天黑前離開山區。

接上台3線後,雖然還是有上上下下的坡,不過跟山裡面的坡比起來,算是有人性多了。

晚上六點半左右,到達北埔預備上寶二的叉路口,這裡決定直接放棄,繼續走台3回家。寶二那段路騎了非常多次了,沒必要在膝蓋狀況如此糟糕的情況下逞強。

回到出發的地方,晚上七點整,整個天色剛好暗下來,非常幸運。

 

計劃 原本預計要走的路線

 

實際 實際走的路線

 

 

騎乘距離:87.2 KM
騎乘時間:4:54:35
平均時速:17.75 km/h

2009年2月24日 星期二

為什麼有些書無法產生共鳴

昨天晚上,終於把去年買的《隱字書》 (原書名 Emdymion Spring, Matthew Skelton著) 讀完了。

這篇不是讀書心得報告,要交作業的可以跳過 XD

書背的折頁寫著,愛爾蘭獨立報的評價是,「這本書將威脅《達文西密碼》的賣座地位……真讓人愛不釋手。」

不論是達文西密碼,還是隱字書,不知道為什麼,看完以後總覺得少了什麼。
書本身不算難看,但總覺得失落,無法產生共鳴,似乎不明白為什麼評價會如此高。

剛剛洗澡的時候,突然領悟到一點:也許是文化的差異。

聖經對於西方人來說,是從小就深深刻印在腦子裡的既成印象,是真理,是與挑戰、質疑絕緣的事物。

也許在西方的國小國中歷史課本,就有教 Gutenberg 是活字印刷的發明人,是為了考試死背下來就對了的人名。

沒有相同文化背景的我們,在看這些書的時候,其實跟看銀河英雄傳說之類的架空小說,感覺並無兩樣。

換個位置想,今天你拿本火鳳燎原給外國人看,他可能分不出來跟北斗神拳的差別。

什麼?沒看過火鳳燎原?那換個例子。

如果有這麼一篇小說,裡面描述那些武將其實都是未來的機器人,劉備曹操孫權其實是坐時光機過去的未來人,諸葛亮是台超級電腦,華陀是一群爆肝死RD組成的團隊,幫關羽刮骨療毒只是售後服務,換根手臂而已。

任何一個中國人都看得出上面這個故事在惡搞,但是如果寫的夠生動,有創意,偶爾再抓幾個梗出來跟史實相呼應,說不定也可以大賣。

如果拿給一個沒讀過三國的外國人看,他會有什麼反應? "關羽? Who is he?"

當你好不容易把三國的歷史介紹完後,他會怎麼寫讀後心得?

"Sango is an advanced dynasty. There are many strong robot generals."

最後說不定還以為楊威利也是三國的人。

另一個無法產生共鳴的原因是,我們國小的課本教的是,發明印刷術的是中國人。

Gutenberg? 誰啊? Coaster? 那是誰? Emdymion Spring? 那又是誰?

2009年2月16日 星期一

Demo 之 Murphy’s Law

"Anything that can go wrong, will"

“如果有件事有可能會出錯,它就一定會出錯。”

 

不只!就連不太可能會出錯的,在關鍵時刻也會出錯!!

 

星期四到台北 Demo,之前已經演練了非常多次,測試了很久的時間,整個影像就是很順。真正到了 Demo 現場,機器架好,很有信心地把程式跑起來。

喵喵樂!為什麼昨天下午還跑得好好的程式,現在開始抖起來了!我的心也跟著抖起來了。

原本很順的畫面,現在變成會回頭;換句話說,就是人往前走,會後退抽一下,再往前走,再後退抽一下;嚴重的時候還會來回抽動好幾下。

把所有的設定全部檢查過,無誤;重新 reboot 再 run,照抖;全部的線重新接過,再抖。

火大換 CODEC,換成原本因為效能考量不 demo 的 H.264,咦?不抖了。見鬼了。

再換回 H.263,咦?又抖了,哪有這樣的啦!

在現場以為前一天 H.263 改到什麼東西,導致整個影像不正常。馬上決定改 demo 會卡卡的 H.264,至少畫面不會跳來跳去。

 

回到新竹後,開始查原因。查出來的時候差點飆淚:原因出在光線。

我們測試的環境,一向是在日光燈底下;當天 demo 現場是鵝黃色美術燈那種燈光,導致 webcam 認為環境很暗;又很巧,webcam 會自動隨著環境的光線調整光量;調整進光量的兩個因素,一個是快門,一個是光圈;webcam 的光圈已經固定,所以能動的只有快門;也就是說,光線越暗,快門的時間越長;快門越長,我們從下指令要從 webcam capture 一張畫面等待的時間要越久。

在每秒鐘 10 張 frame 的情況下,display 每 100ms 就會跑一次,只要 capture 並 encode 一張 frame 的時間超過 100 ms,就有可能發生 capture 一張,display 兩張以上的情況。也就是說,display 跑了兩次,會拿到同樣的東西。

照這麼說來,理論上畫面只會停在最後一個畫面,為什麼會跳來跳去?原因在於,實作 display 的同事,參考的 sample code,使用了 ping-pong buffer;簡單地說,buffer 有兩個,分別存了最後一張和倒數第二張的 frame;display 在存取時,會照著 buffer 1, buffer 2, buffer 1, buffer 2 的順序不斷抓 frame 出來 play。當 decoder 沒有資料寫入,把 buffer 更新時, display 就會一直抓到上一張、上上一張、上一張、上上一張;最後結果就是變成我們看到的,我又跳過來啦,我又跳過去啦,我又跳過來啦,打我啊笨蛋的情況。

 

所以,天時、地利、人和,缺一不可,在 demo 的時候,集所有不可能於一身的情況發生了,它就開始抖了。

 

再次證明了 Murphy’s Law 的真理。

2009年1月23日 星期五

在 Linux 下寫組語, 透過 int 0x80 使用 system call

使用環境:

  • Linux 2.6.24.1
  • yasm 0.7.1.2093
  • GNU ld 2.15.92.0.2 20040927

 

在 Linux 下,要使用 system call,可以透過 int 0x80 來完成。

首先,要先找出 system call 對應的號碼,後面會用到;號碼可以在 sys/syscall.h 中找到。如果看到 syscall.h 裡只有 include 其他 header file,請繼續追著這些 header file 往下找。例如,在我使用的機器上,sys/syscall.h include 了 asm/unistd.h,asm/unistd.h 又呼叫了 asm/unistd_32.h,最後在 asm/unistd_32.h 內找到了如下列的號碼

#define __NR_restart_syscall      0
#define __NR_exit                 1
#define __NR_fork                 2
#define __NR_read                 3
#define __NR_write                4
#define __NR_open                 5
#define __NR_close                6

..... (以下還有好多,省略)

如果你要使用 read() 的 system call,號碼就是 3 號,要用 open(),號碼就是 5 號,其他以此類推。

知道了號碼之後,正式進入如何透過 int 80 來使用 system call 的主題。

先來個 C 的簡單範例。

#include <unistd.h>

int main(int argc, char *argv[])
{
    char str[] = "Hello World!\n";

    write(1, str, sizeof(str));

    return 0;
}

上例中,我們使用了 write,將 str 字串寫到 standard output (就是 write() 的那個 1) ,也就是螢幕上。這個程式可以將字串內的內容顯示在螢幕上。

接著,一模一樣的事,我們使用 assembly code 來實現。

 

section .text
    global _start

_start:
        mov     edx,len ;message length
        mov     ecx,msg ;message to write
        mov     ebx,1   ;file descriptor (stdout)
        mov     eax,4   ;system call number (sys_write)
        int       0x80    ;call kernel

        mov     eax,1   ;system call number (sys_exit)
        int       0x80    ;call kernel

section .data

msg     db      'Hello, world!',0xa,0xd,0       ;our string
len       equ     $ - msg                 ;length of our dear string

 

在使用 int 0x80 之前,必須要將我們剛查出來的號碼填到 eax 內;填好後,只要一使用 int 0x80,相對應的 system call 就會被呼叫。

另外,system call 的參數要依序被放在 ebx, ecx, edx, esi, edi, ebp 等 register 內;以此例來說,一開始,我們使用 sys_write,查表得到號碼是 4 號,於是將此號碼填入 eax;接著,我們要將三個參數搬到對應的 register 內;參考 C 程式,write 有三個參數,分別是 1, string buffer 的位址,及 string buffer 的長度;這三個參數在呼叫 0x80 之後,也必須分別擺到 ebx, ecx, edx 內。全部擺好後,最後使用 int 0x80,噹啷~ Hello, World! 就顯示在螢幕上了。

最後,程式要離開,我們使用了 sys_exit,也就是 1 號的 system call,離開程式。

 

Reference: http://asm.sourceforge.net//intro/hello.html#AEN86