由於之前在各種場合看到別人貼出的bloom特效做的圖片,一開始還以為是用的HDR技術,后來一研究才發現絕大部分都僅僅是一個bloom特效而已,遂打算學習一番。其實bloom是一個非常簡單的后期圖像處理過程,之所以稱其為圖像處理過程,是因為它是一種可以在圖片生成完畢后再使用的后處理過程。那么它到底是什么樣的一種過程呢?簡單地說就是:
Step1. 先對圖片每一像素點進行一個亮度值檢測,若大於某一個閾值就保留其原始顏色值,否則置為黑色;
Step2. 對上一步結果做高斯模糊;
Step3. 將模糊后的圖片和原圖片做一個加權和(權值視具體情況而定);
這里面涉及到的幾個關鍵知識點有必要簡單地說一下:
1.所謂一個像素的亮度值是什么?
亮度值並不是(R+G+B)/3,而是另外一種顏色格式(YUV顏色格式)中的Y值,YUV格式是歐洲電視系統所采用的一種顏色編碼方式,其中"Y"表示明亮度,"U"和"V"表示色彩和飽和度。YUV格式在圖像處理中運用廣泛,比如大家耳熟能詳的灰度圖其實就是存放的每個像素對應的"Y"值。YUV和RGB之間有一個線性的轉換關系:
Y=0.299R+0.587G+0.114B;
U=-0.147R-0.289G+0.436B;
V=0.615R-0.515G-0.100B;
可見G的值對亮度的貢獻最大,而B貢獻最小,這也符合人類視覺系統對於亮度的敏感程度值,因此Step1中的亮度值檢測實際就是判斷(0.299R+0.587G+0.114B)是否大於一個既定閾值。
2.高斯模糊是什么?
圖像模糊的原理很簡單,就是一張圖片中每個像素都取周邊一定范圍內像素的加權和,最簡單的權值設置方法就是均勻求權,即每個像素是這個范圍內所有像素的平均值,如果范圍是3*3,那么權值矩陣為:,而高斯模糊則是利用二維正態分布來生成這個權值矩陣,比如設二維正態分布函數為G(x,y),我們可以讓矩陣正中心那個點值為G(0,0),中心偏左的點為G(-1,0),以此類推,3*3的權值矩陣可以是:
。當然,這里只是隨便舉個例子,實際運用中肯定會比這個復雜得多,因為還要涉及到方差σ的選取等問題。實際上高斯模糊假設了在圖像空間中像素的變化是緩慢的,因此相鄰像素之間的變化不會太明顯,但是隨機的兩個像素間就可能形成很大的像素差,這樣也使高斯模糊能在保留信號的條件下減小噪聲。但是這種方法在接近物體邊緣的時候就無效了,因為圖像中物體邊緣兩個點的顏色差值往往會比較大,所以高斯模糊會把邊緣平滑掉。而另一種雙邊濾波方式則不會把邊緣磨平,但是在bloom中,恰恰相反,我們希望得到一種看起來"顏色外溢"的效果,所以高斯模糊是個理想的選擇。
只要對以上兩點了解后,編寫一個bloom特效程序簡直就是分分鍾的事了,下面是我用OPENCV寫的一個對圖片進行bloom處理的簡單程序:
#include <cv.h>
#include <highgui.h>
#define THRESHOLD_COLOR 0x38
#define A_VALUE 1.0f
#define B_VALUE 1.0f
IplImage *img;
int main(int argc,char** argv) {
IplImage *img = cvLoadImage("chinesedragon.jpg");
cvNamedWindow("Image-in",CV_WINDOW_AUTOSIZE);
//先顯示原jpg圖
cvShowImage("Image-in",img); // 灰度化
IplImage *imggrey=cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
cvCvtColor(img,imggrey,CV_BGR2GRAY);
cvShowImage("Image-grey",imggrey);
IplImage *tmp=cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 3);
cvZero(tmp);
for (int h=0;h<tmp->height;h++) {
uchar *p=(uchar*)(imggrey->imageData+h*imggrey->widthStep);
uchar *ptr_s = cvPtr2D(img, h, 0, NULL);
uchar *ptr_d = cvPtr2D(tmp, h, 0, NULL);
for (int w=0;w<tmp->width;w++) {
if (p[w] >= THRESHOLD_COLOR) {
ptr_d[w*3] = ptr_s[w*3];
ptr_d[w*3+1] = ptr_s[w*3+1];
ptr_d[w*3+2] = ptr_s[w*3+2];
}
}
}
cvShowImage("Image-threshold",tmp);
//分配空間存儲處理后的圖像
IplImage *blur=cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 3);
cvSmooth(tmp,blur,CV_GAUSSIAN,7,7);
cvShowImage("Image-gaussianblur",blur);
// 圖像相加
for (int h=0;h<tmp->height;h++) {
uchar *ptr_s = cvPtr2D(img, h, 0, NULL);
uchar *ptr_d = cvPtr2D(blur, h, 0, NULL);
for (int w=0;w<tmp->width;w++) {
unsigned int r1 = A_VALUE * ptr_s[w*3] + B_VALUE * ptr_d[w*3];// * ptr_d[w*3] / 0xff;
if (r1 > 0xff) r1 = 0xff;
ptr_d[w*3] = r1;
unsigned int r2 = A_VALUE * ptr_s[w*3+1] + B_VALUE * ptr_d[w*3+1];// * ptr_d[w*3+1] / 0xff;
if (r2 > 0xff) r2 = 0xff;
ptr_d[w*3+1] = r2;
unsigned int r3 = A_VALUE * ptr_s[w*3+2] + B_VALUE * ptr_d[w*3+2];// * ptr_d[w*3+2] / 0xff;
if (r3 > 0xff) r3 = 0xff;
ptr_d[w*3+2] = r3;
}
}
cvShowImage("Image-bloom",blur);
//清除垃圾
cvReleaseImage(&imggrey);
cvReleaseImage(&img);
cvReleaseImage(&tmp);
cvWaitKey();
//銷毀窗口
cvDestroyWindow("Image-in");
cvDestroyWindow("Image-grey");
cvDestroyWindow("Image-threshold");
cvDestroyWindow("Image-gaussianblur");
cvDestroyWindow("Image-bloom");
return 0;
}
最后是我用這段程序對我以前渲染的中國龍進行bloom后的效果(下面兩張圖分別是bloom效果圖和原始圖片):
后記:我發現在很多游戲中閾值似乎設置的是0(也可能是它們用了更為復雜的方法),這樣可以避免剛好處於閾值兩端的像素點間顏色不連續的問題。
在網上有一篇講bloom和hdr區別的帖子中(可參考http://game.ali213.net/thread-2075708-1-1.html)用了一幅毀滅戰士3中的圖片,下面是效果比較
(1:游戲中未開啟bloom的效果,2:游戲中開啟bloom的效果,3:圖1經過我的程序跑出來的bloom效果(閾值設為0,A,B兩個value都設為1.0))
可以看出游戲中的泛光現象更加明顯,可能是它用了更寬范圍的高斯模糊(或類似算法)。