图像水波纹特效原理分析和实现

   前段时间注意到一些软件上有图像的水波纹特效,似乎很炫,想深入了解下该效果的具体原理与实现方式,上网搜了不少些资料,都讲得不清不楚,没办法只能靠自己了。花了一整个下午先去复习了高中物理的波的知识,试着自己来推导原理并实现了下。下面的推导是我根据一些资料以及自己分析出的,如有错误,望请指出。上张效果图先:

 

基本原理 


  水波效果反映到图像上,则是像素点的偏移。因此对图像的处理就如同图像缩放一样,对于输出图像的每个点,计算其对应于原始输入图像的像素点,通过插值的方法即可计算其颜色值。 

  先说下关于水波的基础知识。波的传播方向与质点振动方向垂直的为横波,相同则为纵波,水波是横波和纵波的叠加,因此水波在传播时,每个质点存在水平运动和上下运动,合起来就是一个椭圆的圆周运动,如下图的蓝色椭圆。 

  如图,红轴表示水面,对于图像来讲,只有在垂直于成像视线的方向(图中蓝色线段)产生的运动才会引起像素的偏移,而平行于实现方向则可以忽略。那么对于某个点x,其运动轨迹上的每个点都可以分解为与视线平行和垂直的2个方向上的位移,我们只要计算出垂直视线的位移y即可计算出水波导致像素点的偏移位移。 

   假定水波是平面简谐波,则能得到其波动方程

   式中,A是振幅,v是波速,t是时间,lamda是波长, x为距离波源的距离,phi为初始相位。但是随着时间的推移和波的传播,其能量会衰减,即其振幅会随着时间和位置衰减,则上面的公式需修改为:

   函数表征了水波能量的衰减情况,因此该函数必须是关于x和t的单调递减函数,实际水波的递减是什么规律我不清楚,但这里自行设定一个即可,一般的衰减函数有线性衰减和指数衰减,具体好坏只要实验结果才具有说服力。 

  一个波源可以由波长,波速,初始相位和振幅唯一确定,因此对于某个时刻的某个点,我们可以计算出其位移。另一方面,水波是向四面八分同时传播的,对图像坐标系来看,每个点的位移都可以分解为水平与垂直位移,从而就可以确定像素点的精确偏移了。当然,对于多个波源,可以分别考虑,每个点的位移是所有波源引起的位移和。

 

水波纹特效具体实现 


 下面就是具体实现了,这个效果实现应该比较简单的。首先有一个波源对象。

 1 typedef unsigned char BYTE;
 2 #define PI 3.1415926
 3 //一个波源
 4 class CWaveSource
 5 {
 6 public:
 7     CWaveSource(float f32Cx = 100.0f, float f32Cy = 100.0f, 
 8                 float f32MaxAmp = 20.0f, float f32Phase = -PI/2,
 9                 float f32WaveLen = 10.0f, float f32V = 1.0f);
10     ~CWaveSource();
11 
12     //该波源在当前时刻,x,y位置处的振幅
13     void GetAmplitude(int x, int y, float &f32ax, float &f32ay);
14 
15     //波传播到下一时刻,需修改相应的波的状态
16     bool Propagate();
17 
18 private:
19     float m_f32Cx;        //波源中心点坐标
20     float m_f32Cy;
21     float m_f32MaxAmp;    //t时刻中心点最大振幅,t=0最大
22     float m_f32InitPhase; //初始相位
23     float m_f32WaveLen;   //波长
24     float m_f32Velocity;  //波速
25     int   m_nTime;        //时间
26 
27     bool  m_bDisappear;   //该波源是否可以消失,随着时间变化,其能量衰减到很小的时候可以认为其消失了
28     float m_f32CurRadius; //当前时刻波的半径,用于节约计算量的变量
29 };
CWaveSource

这里面注释比较清楚,就不多说了。该对象对应的实现为:

 1 CWaveSource::CWaveSource(float f32Cx, float f32Cy, 
 2                          float f32MaxAmp, float f32Phase, 
 3                          float f32WaveLen, float f32V)
 4 {
 5     m_f32Cx = f32Cx;
 6     m_f32Cy = f32Cy;
 7     m_f32MaxAmp = f32MaxAmp;
 8     m_f32InitPhase = f32Phase;
 9     m_f32WaveLen = f32WaveLen;
10     m_f32Velocity = f32V;
11     m_nTime = 0;
12     m_bDisappear = false;
13     m_f32CurRadius = 1e-12;
14 }
15 CWaveSource::~CWaveSource()
16 {
17 
18 }
19 
20 bool CWaveSource::Propagate()
21 {
22     //下一个时刻
23     m_nTime++;
24     m_f32CurRadius += m_f32Velocity;
25     //考虑最大振幅随时间的衰减,这里的衰减函数可以自己设定
26     //只要振幅随着时间递减即可,最好能做到比较自然
27     m_f32MaxAmp /= 1.01f;
28 
29     //能量衰减到一定程度后,可以认为该波源已消失,通过返回让外面把它删掉
30     if(m_f32MaxAmp < 1e-3)
31     {
32         m_bDisappear = true;
33     }
34     return m_bDisappear;
35 }
36 
37 void CWaveSource::GetAmplitude(int x, int y, float &f32ax, float &f32ay)
38 {
39     f32ax = f32ay = 0.0f;
40     //波源已消失,防止上层未删掉,耗计算量
41     if(m_bDisappear) return;
42     //注:这里可以建一个表,图像区域每个点相对波源中心点位置是固定的,即是f32Dist固定
43     //那么位置对应的衰减系数就应该是固定的
44     //这样对于一个波源只需计算一次f32Dist和衰减系数r了
45     //此处为了代码的可读性,暂时不那样实现    
46     float f32Radius = m_f32CurRadius;
47     float f32Dx = (m_f32Cx - x);
48     float f32Dy = (m_f32Cy - y);
49 
50     float f32Dist = f32Dx * f32Dx + f32Dy * f32Dy;
51     if(f32Dist > f32Radius * f32Radius) return;
52 
53     f32Dist = sqrt(f32Dist);
54     //考虑当前点最大振幅随位置的衰减,这里的衰减函数也可以自己设定
55     float r = 1 - f32Dist / 1000.0f;//exp(-f32Dist/10);
56     float f32MaxAmp = m_f32MaxAmp * r;
57 
58     //计算当前点的振幅,每个点的相位在不停变化的 y = Acos(2*pi*(vt-x)/wavelen + phase)
59     float f32Temp = 2 * PI * (m_f32Velocity * m_nTime - f32Dist) / m_f32WaveLen + m_f32InitPhase;
60     float f32CurAmp = f32MaxAmp * sin(f32Temp);
61 
62     f32ax = f32CurAmp * ABS(f32Dx)/f32Dist;
63     f32ax = f32CurAmp * ABS(f32Dy)/f32Dist;
64 }
View Code

好了,这样一个波源就已经构造好了。对图像的水波特效处理对象如下:

  1 class CRippling
  2 {
  3 public:
  4     CRippling();
  5     ~CRippling();
  6     
  7     //增加一个波源,该函数最好能重载一个直接输波源参数的,这里暂时忽略
  8     void AddSource(CWaveSource &WaveSource);
  9     
 10     //当前已有的波源对图像进行处理
 11     void Process(BYTE *pu8Dst, BYTE *pu8Src, int nWidth, int nHeight, int nChannels = 1);
 12     
 13     //所有波源传播
 14     void Propagate();
 15 
 16 private:
 17     //获取当前时刻所有波源作用下,像素点x,y对应于原始图像的像素位置,返回到x,y中
 18     void GetPos(float &x, float &y);
 19 
 20     //获取图像亚像素值,采用双线性插值
 21     void GetSubPixel(BYTE *pu8Dst, BYTE *pu8Src, int nWidth, int nHeight, int nChannels, float x, float y);
 22 
 23     //波源相关信息,因为需要插入和删除故采用list管理,也可以使用数组或者其他数据结构
 24     list<CWaveSource> m_lWaveSrc;
 25 };
 26 
 27 CRippling::CRippling()
 28 {
 29 }
 30 
 31 CRippling::~CRippling()
 32 {
 33 }
 34 
 35 void CRippling::AddSource(CWaveSource &WaveSource)
 36 {
 37     m_lWaveSrc.push_back(WaveSource);
 38 }
 39 
 40 void CRippling::Process(BYTE *pu8Dst, BYTE *pu8Src, int nWidth, int nHeight, int nChannels)
 41 {
 42     int nRow, nCol;
 43     float x,y;
 44     for(nRow = 0; nRow < nHeight; nRow++)
 45     {
 46         for(nCol = 0; nCol < nWidth; nCol++)
 47         {
 48             x = nCol;
 49             y = nRow;
 50             GetPos(x, y);
 51             GetSubPixel(pu8Dst, pu8Src, nWidth, nHeight, nChannels, x, y);
 52             pu8Dst += nChannels;
 53         }
 54     }
 55 }
 56 
 57 void CRippling::Propagate()
 58 {
 59     //每个波源都需要传播
 60     list<CWaveSource>::iterator It = m_lWaveSrc.begin();
 61     while(It != m_lWaveSrc.end())
 62     {
 63         bool bDisappear = It->Propagate();
 64         //波源可以消失,则删掉
 65         if(bDisappear) It = m_lWaveSrc.erase(It); 
 66         else It++;
 67     }
 68 }
 69 
 70 void CRippling::GetPos(float &x, float &y)
 71 {
 72     //根据平面简谐波的特性,某个点的位移是所有波位移和
 73     float f32AmpX = 0;
 74     float f32AmpY = 0;
 75     list<CWaveSource>::iterator It = m_lWaveSrc.begin();
 76     while(It != m_lWaveSrc.end())
 77     {
 78         It->GetAmplitude(x, y, f32AmpX, f32AmpY);
 79         x += f32AmpX;
 80         y += f32AmpY;
 81         It++;
 82     }
 83 }
 84 
 85 void CRippling::GetSubPixel(BYTE *pu8Dst, BYTE *pu8Src, int nWidth, int nHeight, int nChannels, float x, float y)
 86 {
 87     //采用双线性插值,就实际效果来看,最近邻插值也可以的
 88     int x0 = x;
 89     int y0 = y;
 90     int x1 = x0 + 1;
 91     int y1 = y0 + 1;
 92 
 93     float wx1 = x - x0;
 94     float wy1 = y - y0;
 95     float wx0 = 1.0f - wx1;
 96     float wy0 = 1.0f - wy1;
 97 
 98     x0 = MAX(x0, 0);
 99     x1 = MAX(x1, 0);
100     y0 = MAX(y0, 0);
101     y1 = MAX(y1, 0);
102 
103     x0 = MIN(x0, nWidth-1);
104     x1 = MIN(x1, nWidth-1);
105     y0 = MIN(y0, nHeight-1);
106     y1 = MIN(y1, nHeight-1);
107     for(int i = 0; i < nChannels; i++)
108     {
109         u8 * pu8Y0 = pu8Src + y0 * nWidth * nChannels;
110         u8 * pu8Y1 = pu8Src + y1 * nWidth * nChannels;
111 
112         float sum_y0 = (pu8Y0[x0 * nChannels + i] * wx0 + pu8Y0[x1 * nChannels + i] * wx1);
113         float sum_y1 = (pu8Y1[x0 * nChannels + i] * wx0 + pu8Y1[x1 * nChannels + i] * wx1);
114         pu8Dst[i] = (BYTE)(sum_y0 * wy0 + sum_y1 * wy1);
115     }
116 }
View Code

测试代码采用隔一定时间增加一个波源,可以产生动态效果。其中调用了opencv读写和显示图像,相关的头文件和库自己加上即可。

 1 void test_rippling()
 2 {    
 3     Mat mImg = imread("C:/test2.jpg");
 4     imshow("org", mImg);
 5     CRippling rippling;
 6     for(int t = 0; t < 10000; t++)
 7     {
 8         if(t % 20 == 0)
 9         {
10             CWaveSource wavesource(50 + rand() % 800, 50 + rand() % 300);
11             rippling.AddSource(wavesource);
12         }
13 
14         printf("\rtimes: %d", t);
15         Mat mSrc = mImg.clone();
16         Mat mDst = mImg.clone();
17 
18         rippling.Process(mDst.data, mSrc.data, mSrc.cols, mSrc.rows, 3);
19         rippling.Propagate();
20         imshow("pro", mDst);
21         waitKey(1);
22     }
23     waitKey(0);
24 }
View Code

这样基本就完成了,可以实现最开始那张图的效果了。

 

总结 


  上面的分析与实现只是按照原始的产生与传播过程来做的,可以想象,随着波源数的增加,算法的耗时会不断增加,当然在代码注释中也在有的地方说明了可以优化的点。另外,如果我们仅仅是实现这样一个效果,很多波源参数给固定下来,那么可以在算法上进一步推导,从而简化运算。比如,固定波速与波长,让波的周期是4个或2个单位之间,那么应该可以推导出后一时刻位移为前面几个时刻位移的关系,这样就不用每次计算sin函数了。

  当然上面的推导只考虑了简单情况,特别地,成像一般会近大远小,因此在图像上下方波的扩张速度和质点偏移都是不一样的,对于完整的模型这些都是应该要考虑的因素。

 

 


 

posted @ 2015-02-08 00:15  过客冲冲  阅读(3820)  评论(1编辑  收藏  举报