由于之前在各种场合看到别人贴出的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))
可以看出游戏中的泛光现象更加明显,可能是它用了更宽范围的高斯模糊(或类似算法)。