這篇教學最主要的目的,並不是希望可以寫出什麼取代掉Lightroom之類的重量級商業的程式,那些有規模的商業軟體基本上已經有相當的成熟度,使用的演算法和最後出來的畫質效果也難以被取代,撰寫這篇教學主要的目的,是想藉由直接撰寫程式來解raw,透過程式的處理過程,能讓大家對於raw的概念.內部技術.相關計算方法有更深入的了解,也一舉終結網路上許多的疑問與誤解,直接徹底剖析raw的奧秘,倒也不是說要寫出什麼更目前上更棒的軟體或是開發出更好的演算法(雖然這的確是很有趣的挑戰...),當然牽涉到程式撰寫,閱讀者就一定要有基本的程式撰寫能力底子在,倒也不是說需要有多強的coding能力,而是至少可以看得懂程式大概在作什麼就夠了(這邊使用的語言是C/C++,相信很多人應該都有學習過這套語言的經驗).
首先先說一下什麼是影像raw檔
http://en.wikipedia.org/wiki/Raw_image_format
,基本上就是儲存數位相機最原始影像數據格式的儲存檔案,不同的相機可能會使用不同的檔案封裝方式,不過大略上組成是都大同小異的,檔案內的內容可能包括有拍攝時相關的條件參數記錄數據(像是拍攝時間.使用光圈.iso.快門.GPS位置.拍攝模式.閃光燈有無與模式.....)
. 影像原始數據(當然這是最大的重點) . 預覽圖片(通常RAW檔會附一個小張的JPG檔供快速預覽用) .
以及一些各家廠商記錄的修正參數資料(這些參數會用來修正最後的輸出成像) , 大體上就區分成這幾個部分, 這幾個部分儲存在檔案中的位置(adder)
每家都不一樣,說真的沒有spec有點難分析,不過就我了解多數檔案儲存規格多是由tiff封裝架構修改而來的就是...
一般人想要自己處理raw檔最大的阻礙,以我自己個人的經驗來說,應該就是正確截取檔案本身數據資料的問題,因為架構未知,要從哪個位置開始讀資料,然後資料以什麼排列的方式組合都是未知,由於根本抓不出正確的影像資料格式,根本無法談更後續的處理,我曾經考慮使用DNG方案,任何影像RAW檔都可以轉換成DNG
http://en.wikipedia.org/wiki/Digital_Negative 檔案封裝方式,再藉由DNG SDK處理
http://www.adobe.com/support/downloads/dng/dng_sdk.html
, 無奈後來發現DNG SDK所提供的操作太高階了,無法處理到最底層的原始數據,因此改換跑掉研究一款open sources的解raw軟體 dcraw
http://www.cybercom.net/~dcoffin/dcraw ,
基本上我是利用它來抓取我要的原始影像數據資料,當然它原來並不提供這個功能,我有修改了它部分原始碼,如底下
unsigned CLASS pana_bits (int nbits) { static uchar buf[0x4000]; static int vbits; int byte; if (!nbits) return vbits=0; if (!vbits) { fread (buf+load_flags, 1, 0x4000-load_flags, ifp); fread (buf, 1, load_flags, ifp); } vbits = (vbits - nbits) & 0x1ffff; byte = vbits >> 3 ^ 0x3ff0; return (buf[byte] | buf[byte+1] << 8) >> (vbits & 7) & ~(-1 << nbits); } void CLASS panasonic_load_raw() { int row, col, i, j, sh=0, pred[2], nonz[2]; /// FILE *fp; FILE *fp_txt; /// printf("%d %d\n", height , raw_width ); pana_bits(0); for (row=0; row < height; row++) for (col=0; col < raw_width; col++) { if ((i = col % 14) == 0) pred[0] = pred[1] = nonz[0] = nonz[1] = 0; if (i % 3 == 2) sh = 4 >> (3 - pana_bits(2)); if (nonz[i & 1]) { if ((j = pana_bits(8))) { if ((pred[i & 1] -= 0x80 << sh) < 0 || sh == 4) pred[i & 1] &= ~(-1 << sh); pred[i & 1] += j << sh; } } else if ((nonz[i & 1] = pana_bits(8)) || i > 11) pred[i & 1] = nonz[i & 1] << 4 | pana_bits(4); if (col < width) if ((BAYER(row,col) = pred[col & 1]) > 4098) derror(); } printf("輸出處理...\n"); //////////////////////////////////////// fp = fopen ("sample_binrary.dat","wb"); fp_txt = fopen ("sample.dat","wb"); for (row=0; row < height; row++) { for (col=0; col < raw_width; col++) { fputc( (BAYER(row,col) & 0xff00)>>8, fp); fputc( BAYER(row,col) & 0xff , fp); } fprintf(fp, "\n"); } for (row=0; row < height; row++) { for (col=0; col < raw_width; col++) { fprintf(fp_txt,"%4d ", BAYER(row,col) ); } fprintf(fp_txt, "\n"); } fclose(fp); fclose(fp_txt); //////////////////////////////////////// printf("輸出完畢."); exit(0); }
紅色部分的程式碼是我自己加上去的,目的是將dcraw paser過後的影像數據匯出到別的檔案內,因為我的相機是GF1,所以我只針對panasonic的RAW檔(*.rw2)來匯出數據,只要你的raw檔是panasonic相機輸出的,應該都都有用.
分別匯出兩個檔案 sample_binrary.dat 跟 sample.dat , sample.dat可以用CSV讀取的方式來讀取影像資料數據,而sample_binrary放置的內容則是二進位數據資料,基本上一個pixel位置用兩個char(16bits)來放置資料,每一行輸出完畢,進入下一列的時候輸出
\n 字元來區隔.
修改版的dcraw和原始碼再最後會有連結提供大家下載(本文後續所使用到的相關範例與檔案也一樣),可以在VC 6.0下編譯通過(為了要在VC
6.0下編譯通過,我有修改了程式開頭的一些設定定義).
接著基於方便的理由(這是我自己個人習慣性的問題,每個人做法不同,僅供參考...),我將sample_binrary.dat的資料重新封裝到BMP格式內(基本上由於並沒有真正放置BMP檔正確內容,所以也無法讀取到正確影像),用我自己寫的BMP讀取LIB來進行RAW檔資料讀取操作.
底下是匯出的處理paser
#include <iostream> #include "bmpsave.h" #include <stdlib.h> #include <stdio.h> #include <math.h> using namespace std; int main( int argc, char **argv ) { unsigned int * raw_data; unsigned biWidth = 4060; unsigned biHeight = 2688; unsigned int c = 0 ; SaveBMP result("decode.bmp"); result.initBMP(4060,2688); FILE *fp; raw_data = new unsigned int [biWidth*biHeight]; fp = fopen ("sample_binrary.dat" , "rb" ); for(unsigned int i=0;i<biWidth*biHeight;i++) { raw_data[c] = ((fgetc(fp)) <<8 | (fgetc(fp))); result.put_pixel( i%4060 , (int) (i/4060) , raw_data[c] ); c++; if( (i+1) % 4060 == 0 ) fgetc(fp) ; } fclose(fp); result.Save(); system ("pause"); return 1; }
目前處理的長寬是寫死的,確定適用於GF1拍攝的3:2畫面比例RAW檔,如果是別的尺寸,你需要修改程式長寬的設定. bmpsave
這個東西是我自己以前寫的函式庫,基本上影像數據原始一個pixel是12bits(GF1是這樣的),存放在bmp封裝一個pixel
24bits的長度內,並沒有任何損失.
最後一步就是處理實際將影像數據還原的步驟了...
在次之前有一些觀念補充,多數的數位相機使用Bayer格式
http://en.wikipedia.org/wiki/Bayer_filter
來儲存影像資料,這種格式的特性是,照理來說一個pixel的位置應該要有RGB三個資訊,才能完整顯色,而Bayer格式則是一個 pixel位置只放置
R或是G或是B其中一個資訊,如果我們要正確顯示影像,則每個pixel的位置我們要將缺少的兩個資訊填補回來,這個過程稱為 demosaicing
http://en.wikipedia.org/wiki/Demosaicing
,根據dcraw內附的說明,據說bayer只會有四種排列的模式
BG GR
GR BG
GB RG
RG GB
以GF1來說它的儲存格式是下面這個
GR BG
填補方法可以參考
http://www.siliconimaging.com/RGB%20Bayer.htm
,我的作法大致是大同小異,基本上缺少的資訊就從旁抓取最近的數據平均來填補(這是很爛很笨卻很簡單的演算法,弄出來的畫面品質soso....).
除了demosaicing外,其實還有很多步驟是解raw需要的(或是說能夠讓解出來的畫面品質更完善),不過在此因為只是教學與研究,目前也只有進行到
demosaicing和白平衡,至少有這兩個步驟後,算是可以展現出可以看的圖片(僅止可以看而以),白平衡處理演算法我是借用
http://pippin.gimp.org/image_processing/chapter-automaticadjustments.html#section-automaticwhitebalance
其中的 Component stretching
,老實說這方法簡單歸簡單,方便歸方便,特定狀況下效果也不錯,不過這麼簡單的演算法,也常常會出包就是,不過至少就一個畫面中包含了可以參考的白色參考面積而且沒有過曝情況下,使用這個方法出來效果還ok.
接著來看我的程式碼
#include <iostream> #include "bmpread.h" #include "bmpsave.h" #include <stdlib.h> #include <math.h> using namespace std; int main( int argc, char **argv ) { LoadBMP bmp( "decode.bmp" ); SaveBMP result("decode-2.bmp"); result.initBMP(4058,2686); //計白平衡處理 unsigned int min_r=4095,max_r=0,min_g=4095,max_g=0,min_b=4095,max_b=0; for(unsigned int i=0 ; i<4000; i++) for(unsigned int j=0; j<2600; j++) { if( abs( (int) i- (int) j ) % 2 == 0 ) { if ( bmp.get_pixel(i+1,j+1) < min_g && bmp.get_pixel(i+1,j+1) != 0 ) min_g = bmp.get_pixel(i+1,j+1); if ( bmp.get_pixel(i+1,j+1) > max_g ) max_g = bmp.get_pixel(i+1,j+1); } if( j%2 == 0 && i%2==1 ) { if ( bmp.get_pixel(i+1,j+1) < min_b && bmp.get_pixel(i+1,j+1) != 0 ) min_b = bmp.get_pixel(i+1,j+1); if ( bmp.get_pixel(i+1,j+1) > max_b ) max_b = bmp.get_pixel(i+1,j+1); } if( i%2 == 0 && j%2==1 ) { if ( bmp.get_pixel(i+1,j+1) < min_r && bmp.get_pixel(i+1,j+1) != 0 ) min_r = bmp.get_pixel(i+1,j+1); if ( bmp.get_pixel(i+1,j+1) > max_r ) max_r = bmp.get_pixel(i+1,j+1); } } printf("min_r:%d max_r:%d\nmin_g:%d max_g:%d\nmin_b:%d max_b:%d\n",min_r,max_r,min_g,max_g,min_b,max_b); system ("pause"); // demosaicing 處理 for(unsigned int i=0 ; i<4058; i++) for(unsigned int j=0; j<2686; j++) { unsigned int r,g,b; if( abs( (int) i- (int) j ) % 2 == 0 ) { g = bmp.get_pixel(i+1,j+1); //result.put_pixel( i,j , (0 | 255<<8 |0<<16 ) ); } else { g = (int) ( bmp.get_pixel(i,j+1) + bmp.get_pixel(i+2,j+1) + bmp.get_pixel(i+1,j+2) + bmp.get_pixel(i+1,j)) / 4 ; } if( j%2 == 0 && i%2==1 ) { b = bmp.get_pixel(i+1,j+1); //result.put_pixel( i,j , (0 | 0<<8 |255<<16 ) ); } else { if( i%2 == 0 && j%2==1 ) b = (int) ( bmp.get_pixel(i,j) + bmp.get_pixel(i,j+2) + bmp.get_pixel(i+2,j) + bmp.get_pixel(i+2,j+2)) / 4 ; if( abs( (int) i- (int) j ) % 2 == 0 && i%2 == 0 )//¤¤間 { b = (int) ( bmp.get_pixel(i,j+1) + bmp.get_pixel(i+2,j+1) ) / 2 ; //result.put_pixel( i,j , (0 | 255<<8 |0<<16 ) ); } if( abs( (int) i- (int) j ) % 2 == 0 && i%2 == 1 ) { b = (int) ( bmp.get_pixel(i+1,j) + bmp.get_pixel(i+1,j+2) ) / 2 ; //result.put_pixel( i,j , (0 | 255<<8 |0<<16 ) ); } } if( i%2 == 0 && j%2==1 ) { r = bmp.get_pixel(i+1,j+1); //result.put_pixel( i,j , (255 | 0<<8 |0<<16 ) ); } else { if( j%2 == 0 && i%2==1 ) { r = (int) ( bmp.get_pixel(i,j) + bmp.get_pixel(i,j+2) + bmp.get_pixel(i+2,j) + bmp.get_pixel(i+2,j+2)) / 4 ; } if( abs( (int) i- (int) j ) % 2 == 0 && i%2 == 1 ) r = (int) ( bmp.get_pixel(i,j+1) + bmp.get_pixel(i+2,j+1) ) / 2 ; if( abs( (int) i- (int) j ) % 2 == 0 && i%2 == 0 ) r = (int) ( bmp.get_pixel(i+1,j) + bmp.get_pixel(i+1,j+2) ) / 2 ; } //result.put_pixel( i,j , (r>>4 | (g>>4)<<8 | (b>>4)<<16 ) ); result.put_pixel( i,j , ( (unsigned int)( 4095.0 * (double)(r-min_r)/((double)(max_r-min_r)) ) >>4 | ( (unsigned int)( 4095.0 * (double)(g-min_g)/((double)(max_g-min_g)) ) >>4)<<8 | ( (unsigned int)( 4095.0 * (double)(b-min_b)/((double)(max_b-min_b)) ) >>4)<<16 ) ); } result.Save(); system ("pause"); return 1 }
註解"白平衡處理"那邊所做的事情簡單來說就是各找出RGB最大與最小的數值,並且記錄下來,最後影像匯出前會藉由這些數據來拉伸RGB數值,至於註解"demosaicing
處理"所做的事情就是就是填補RGB缺乏的資訊,只是這樣而已.
最後輸出前對RGB做重新的拉伸,然後因為格式的關係,原本RGB 12:12:12的數據資料,轉換成RGB 8:8:8共24bits封裝在bmp內.
或許大家會有疑問,明明GF1 3:2正確的長寬應該是4000*2672,怎麼在這邊變成了4060*2688??
其實應該這麼說,原始數據的確是給4060*2688,但是在正確完整的解RAW動作下,會進行一些額外處理,像是變形校正,經過校正後,畫面的可視範圍自然會變小,再經過裁切,就是我們所看到的4000*2672了,此外我猜拉...因為解RAW最邊緣的PIXEL會再用到鄰近pixel去做填補的計算,我們也自然不可能真的拿最大的可視畫面去處理(好比以我的處理,我是拿4058*2686去處理),這些因素下,我們需要有oversample的數據去做處理(此外有些邊界地帶雖然有資料,但是資料都是
0 ,可視為根本是bad pixel,也要切除掉,我就是因為這關係所以在尋找rgb min數值時把0給剔除了 ).
那這樣解raw解完了嗎?
NO!!!
其實說真的還省略很多步驟,但是這些步驟其實也可以擺在輸出後的後製階段處理,基於只是研究摸索與教學的原因,就沒繼續實作了,舉例來說.去躁.變形校正.對比亮度調整.bad
pixel遮蔽.銳化.彩度調整.色溫調整.Gamma調整.....都可以算是解raw的一環,簡單來說你認為的很多後製階段所做的事情,其實也可以是解raw的環節,不過最重要的白平衡跟demosaicing若是沒做,解raw都不能稱為是解raw了.....
除了Bayer放置RGB外,其實還有其他少數不同的數據資料,像是Foveon影像sensor陣列的話,一個PIXEL就會有完整的RGB資訊,還有幾款canon的dc使用的是
http://en.wikipedia.org/wiki/CYGM_filter ,但是大體來說Bayer算是最普及也最常見的方式就是.
然後來看看成果吧....
老實說成果不是很好....因為白平衡沒處理的很好,也還沒經過Gamma校正的關係,不過這的確是自己解出來的結果.
留言列表