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

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

基本原理



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

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

  如图,红轴表示水面,对于图像来讲,只有在垂直于成像视线的方向(图中蓝色线段)产生的运动才会引起像素的偏移,而平行于实现方向则可以忽略。那么对于某个点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 }

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

  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 }

测试代码采用隔一定时间增加一个波源,可以产生动态效果。其中调用了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 }

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

总结 



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

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



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

时间: 2024-10-21 22:41:09

图像水波纹特效原理分析和实现的相关文章

2.CCGridAction(3D效果),3D反转特效,凸透镜特效,液体特效,3D翻页特效,水波纹特效,3D晃动的特效,扭曲旋转特效,波动特效,3D波动特效

 1 类图组织 2 实例 CCSprite * spr = CCSprite::create("HelloWorld.png"); spr->setPosition(ccp(winSize.width/2,winSize.height/2)); addChild(spr); //GridAction //CCFlipX3D * action = CCFlipX3D::create(2); //CCFlipY3D * action = CCFlipY3D::create(2);

超酷的计步器APP——炫酷功能实现,自定义水波纹特效、自定义炫酷开始按钮、属性动画的综合体验

超酷的计步器APP--炫酷功能实现,自定义水波纹特效.自定义炫酷开始按钮.属性动画的综合体验 好久没写博客了,没给大家分享技术了,真是有些惭愧.这段时间我在找工作,今年Android的行情也不怎么好,再加上我又是一个应届生,所以呢,更是不好找了.但是我没有放弃,经过自己的不懈努力,还是找到了自己喜欢的Android工作,心里的一块石头终于落下了.但是迎接我来的是更多的挑战,我喜欢那种不断的挑战自我,在困难中让自己变得更强大的感觉.相信阳光总在风雨后,因为每一个你不满意的现在,都有一个你没有努力的

Android5.0以上的项目都会有的按钮点击特效--水波纹

<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <solid android:color="@color/colorPrimary"/> <corners android:radius="10dp" /&

Android特效专辑(十)——点击水波纹效果实现,逻辑清晰实现简单

Android特效专辑(十)--点击水波纹效果实现,逻辑清晰实现简单 这次做的东西呢,和上篇有点类似,就是用比较简单的逻辑思路去实现一些比较好玩的特效,最近也是比较忙,所以博客更新的速度还得看时间去推演,但是也能保证一周三更的样子,现在也还是以小功能,或者说是一些小入门级别的博客为主,我也不算是什么很厉害的人,很多细节的支持处理的仍然还是不到位,所以也是一直在弥补,话不多说,来看看今天的效果 实现起来很简单吧,那我们就来看一下他是怎么实现的咯! OnclickRuning package com

图像特征算子--之灰度共生矩阵原理分析与实现(八)

图像特征算子--之灰度共生矩阵原理分析与实现(八) [email protected] http://blog.csdn.net/kezunhai 灰度共生矩阵最早由Robert M.在提出,早期称为灰度空间依赖矩阵(Gray-Tone Spatial-Dependence Matrices),在其论文里,根据灰度空间依赖矩阵可以计算28种纹理特征,详细内容可以参考:Textural Features for Image Classification.由于纹理是由灰度分布在空间位置上反复出现而形

一个2d的水波纹的特效 脚本

2d游戏里的一些特效,都是可以借助摄像机和面板直接的距离等的问题,进行多加控制的,贴出以脚本.不过Texture的话,是一些列的水波纹的那种,我是实在找不到了=_= . using System.Collections; using System.Collections.Generic; using UnityEngine; ///利用摄像机到Canvas的距离 放置Panel public class EF_waterWave : MonoBehaviour { public Texture[

HTML5 Canvas水波纹动画特效

HTML5的Canvas特性非常实用,我们不仅可以在Canvas画布上绘制各种图形,也可以制作绚丽的动画,比如这次介绍的水波纹动画特效.以前我们也分享过一款基于HTML5 WebGL的水波荡漾动画,让人惊叹不已,这次分享的HTML5 Canvas水波纹动画同样非常震撼人心. 在线演示          源码下载 <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8&q

自定义控件三部曲之绘图篇(六)——Path之贝赛尔曲线和手势轨迹、水波纹效果

前言:好想义无反顾地追逐梦想 相关文章:<Android自定义控件三部曲文章索引> 从这篇开始,我将延续androidGraphics系列文章把图片相关的知识给大家讲完,这一篇先稍微进阶一下,给大家把<android Graphics(二):路径及文字>略去的quadTo(二阶贝塞尔)函数,给大家补充一下. 本篇最终将以两个例子给大家演示贝塞尔曲线的强大用途: 1.手势轨迹 利用贝塞尔曲线,我们能实现平滑的手势轨迹效果 2.水波纹效果 电池充电时,有些手机会显示水波纹效果,就是这样

Android自定义控件-Path之贝赛尔曲线和手势轨迹、水波纹效果

从这篇开始,我将延续androidGraphics系列文章把图片相关的知识给大家讲完,这一篇先稍微进阶一下,给大家把<android Graphics(二):路径及文字>略去的quadTo(二阶贝塞尔)函数,给大家补充一下. 本篇最终将以两个例子给大家演示贝塞尔曲线的强大用途: 1.手势轨迹 利用贝塞尔曲线,我们能实现平滑的手势轨迹效果 2.水波纹效果 电池充电时,有些手机会显示水波纹效果,就是这样做出来的. 废话不多说,开整吧 一.概述 在<android Graphics(二):路径