前些天做圖像識別的題,深感自己數學推算水平下降了。所以,今天嘗試一下一個簡單的圖像處理算法:通過數學建模方法生成水波紋效果。波紋只考慮折射,不考慮反射。
算法推導
首先,從簡單的開始。我們先嚐試一下水平縱波,方向沿y軸向下,波的表達式
注意的是這裏從計算速度角度來看,不使用惠更斯原理,只使用簡單的幾何光學。討論的情景是水底的自發光景物發出平行光向上射出水面,投影到水面外的屏幕上。簡單起見,屏幕假設在水波最高點處。
畫圖如下:
入射角與切平面與水平面斜率相同,爲正弦函數導數,即:
這裏k爲切平面與水平面夾角,ω爲角速度(弧度rad),A爲振幅,x爲行程,v爲波速,t爲時間。
入射角與切平面夾角一致爲k。根據折射定律,入射角和折射角滿足:
這樣,根據解析幾何,可以知道:
有了這些,基本的水波就能畫出來了。我們先完成基礎功能,更加複雜的模擬,如衰減、反射,先暫時不添加。
接下來就是將算法改寫爲具體的程序。我們使用remap函數。Remap函數的作用是把一幅圖像中某位置的像素放置到另一個圖片指定位置的過程。
void remap(InputArray src, OutputArray dst, InputArray map1, InputArray map2,
int interpolation,int borderMode = BORDER_CONSTANT,
const Scalar&borderValue = Scalar())
第一個參數:輸入圖像,即原圖像,需要單通道8位或者浮點類型的圖像
第二個參數:輸出圖像,即目標圖像,需和原圖形一樣的尺寸和類型
第三個參數:它有兩種可能表示的對象:(1)表示點(x,y)的第一個映射;(2)表示CV_16SC2,CV_32FC1等
第四個參數:它有兩種可能表示的對象:(1)若map1表示點(x,y)時,這個參數不代表任何值;(2)表示CV_16UC1,CV_32FC1類型的Y值
第五個參數:插值方式,有四中插值方式:1)INTER_NEAREST——最近鄰插值
2)INTER_LINEAR——雙線性插值(默認)
3)INTER_CUBIC——雙三樣條插值(默認)
4)INTER_LANCZOS4——lanczos插值(默認)
第六個參數:邊界模式,默認BORDER_CONSTANT
第七個參數:邊界顏色,默認Scalar()黑色
最基本的波:平行波
最簡單的,是水平傳播的平行波。這裏mapx.col(x)=x;的作用是將第x行全部設置爲x。
voidMoire_HorizonWave( const Mat &src, Mat &dst, float A, float w, floatfai, float n )
{
Mat mapx( src.rows, src.cols, CV_32FC1 ),mapy( src.rows, src.cols, CV_32FC1 );
int cnt = 0;
for ( auto y = 0u; y <src.rows ; y++ )
{
double alpha = atan( cosf( w*y -fai ) );//切線與x軸夾角,相當於入射角
double gamma = asin( sinf( alpha )/ n );//折射角
double val = y - A* tan( alpha -gamma );
mapx.col(x) = x;
mapy.row(y) = val;
}
remap( src, dst, mapx, mapy,INTER_LINEAR, cv::BORDER_WARP );
}
效果如下,幀時間約45ms(DEBUG模式,以下分析效率時,不加說明都是DEBUG模式。而RELEASE模式一般耗時是DEBUG模式一半。
而將這一情況推廣到任意方向的平行波時,這一方法就不能使用了。此時,行程x不再是x座標,而是採樣點與起始直線之間的有向距離。當直線前進方向(法向量)的角度爲dir時,直線方程滿足(簡單起見,直線過(0,0)點):
因此,我們的函數原型如下:
for (int y = 0; y < src.rows; y++ )
{
for ( int x = 0; x < src.cols;x++ )
{
double k = tan( ang ), divk = sqrt( 1 +k*k );
double dist = ( -k*x + y ) /divk;//點與波紋方向直線的距離
double alpha = atan( cos(w*dist + fai ) );//切線與x軸夾角,相當於入射角
double gamma = asin( sin( alpha )/ n );//折射角
double val = - A* tan( alpha -gamma );
mapx.at<float>( y, x ) =x+val*cos( ang );
mapy.at<float>( y, x ) =y+val*sin( ang );
}
}
但是這樣效率非常低,幀時間約260ms,表現出來就是感覺非常不流暢。所以,我們必須進行優化。
第一次優化:穩定變量外移、低精度浮點、指針運算
觀察可知,在遍歷過程中,由於前進方向處處相等,因此很多變量的值是不變的。而這些運算都是三角函數,計算量很大,將這些量提出來能夠大大減少計算量。
此外,由於我們的基本單位是“像素”,並不需要很高的精度,完全可以將其替換爲浮點數版本。(sinf,cosf,tanf,atanf……)從而大大提高速度。
而第三種就是很基礎的指針運算替換at()方法。這方面的效率差別最高能相差一半。此外,由於我們的轉換矩陣mapx,mapy是連續的內存,因此只需要一直進行指針自加不需要用ptr方法獲得行首地址指針。
float k= tanf( ang ), divk = sqrtf( 1 + k*k );
floatcosang = cosf( ang ), sinang = sinf( ang );
float*px = (float*)mapx.data, *py = (float*)mapy.data;
for (int y = 0; y < src.rows; y++ )
{
for ( int x = 0; x < src.cols;x++ )
{
float dist = ( -k*x + y ) /divk;//點與波紋方向直線的距離
float alpha = atanf( cosf(w*dist + fai ) );//切線與x軸夾角,相當於入射角
float gamma = asinf( sinf( alpha )/ n );//折射角
float val = - A* tanf( alpha -gamma );//= y + A*tanf( 2 * alpha );
*px++ = x + val*cosang;
*py++ = y + val*sinang;
}
}
稍作優化,瞬間幀率加倍,一幀140ms,改成release模式則只需76ms。可見優化的效率。
第二次優化:查表法
再要優化,那簡單的替換就沒有用了。我們再觀察一下我們的推導公式:
可以看出:
1. 相同參數下,不同的行程即 對應不同的 。
2. 是一個週期函數,只需要計算0~2π之內的值就可以了。
3. 對於不同的參數, 和v隻影響行程ωx-vt,而A只對 起到一個線性縮放的作用。唯有n的變化會對 造成非線性的影響。
所以,當n固定之後,可以預先計算出不同的偏差值 表,之後對於不同的採樣點,只需要查表插補再乘上A線性縮放即可,從而完全避免三角函數運算。
表生成函數如下。這裏加入靜態變量La和ln,是爲了避免每次都進行查表運算:
const int TBL_SIZE_MUL = 1;
const int TBL_SIZE = 360 * TBL_SIZE_MUL;
float deltaTbl[TBL_SIZE];
void InitRefractionTbl (float A,float n)
{
static float lA = 0, ln = 0;
if( A != lA || n != ln )//如果A、n改變則重新生成,不考慮浮點誤差
{
for( auto i = 0u; i < sizeof( deltaTbl ) / sizeof( deltaTbl[0] ); i++ )
{
floatalpha = atanf( cosf( 2 * i*PI / ( sizeof( deltaTbl ) / sizeof( deltaTbl[0] ) )) );//切線與x軸夾角,相當於入射角
floatgamma = asinf( sinf( alpha ) / n );//折射角
floatval = -A* tanf( alpha - gamma );//折射後偏差值
deltaTbl[i]= val;
}
lA= A, ln = n;
}
}
inline float GetRefraction ( float dist )
{
float remain = fmodf( dist, 2 * PI );//用角度表示的行程,並取餘,應當爲從
if( remain < 0 )remain += 2 * PI;
else remain = 0;//爲正
return deltaTbl[static_cast<int> ( remain / 2 / PI* TBL_SIZE )];//查表,精度略有損失
}
//ang是前進方向與水平軸夾角,單位爲弧度
void Moire_ParallelWave( const Mat&src, Mat &dst, float rad, float A, float w, float fai, float n )
{
Mat mapx(src.rows, src.cols, CV_32FC1 ), mapy( src.rows, src.cols, CV_32FC1 );
InitRefractionTbl(n );
float k = tanf( rad ), divk = sqrtf( 1 + k*k );
float *px = (float*)mapx.data, *py = (float*)mapy.data;
for ( int y = 0; y < src.rows; y++ )
{
for ( int x = 0; x < src.cols; x++ )
{
//查表法,效率高的方法
float dist = ( -k*x + y ) / divk;//點與波紋方向直線的距離
float delta = A* GetRefraction( w*dist + fai );
*px++ = x + delta*cosrad;
*py++ = y + delta*sinrad;
}
}
remap( src, dst,mapx, mapy, INTER_LINEAR, cv::BORDER_REPLICATE );//DEBUG下大約3ms數量級
}
優化之後,幀時間減少到了約75ms,而且更重要的是通用性得到了很大的提高。
進化:環形波
環形波比平行波要複雜一些,主要區別是其行程由採樣點和直線之間的距離變成了採樣點和圓心之間的距離:
有了之前的程序,環形波就很簡單了,只需要將dist進行一點計算就行了。
void Moire_CircleWave( const Mat &src,Mat &dst,const Point2f O,float cost, float A, float w, float fai, float n )
{
Mat mapx( src.rows, src.cols, CV_32FC1 ), mapy( src.rows, src.cols, CV_32FC1 );
InitRefractionTbl(n );
float*px = (float*)mapx.data, *py = (float*)mapy.data;
for( int y = 0; y < src.rows; y++ )
{
for( int x = 0; x < src.cols; x++ )
{
floatdist = sqrtf( ( x - O.x )*( x - O.x ) + ( y - O.y )*( y - O.y ) );//點與波紋方向直線的距離
floatdelta = A* GetRefraction( w*dist + fai );
floate_cost = ::expf( -cost*dist );//隨距離衰減係數
*px++= x + e_cost*delta* ( x - O.x ) / dist;
*py++= y + e_cost*delta* ( y - O.y ) / dist;
}
}
remap(src, dst, mapx, mapy, INTER_LINEAR, cv::BORDER_REPLICATE );
}
這樣的速度約1幀90ms。效果如下:
進化:波的干涉
以上,基本的波都完成了。但是這樣屏幕上只能有單個波,然而一個平面上只有單個波的情況在現實中非常少,我們勢必要引入波的干涉。
波的干涉滿足線性疊加定理:
所以,只需要將所有的波疊加即可。這就需要將不同的波記錄並運算。一個直接的想法是建立一個控制器類,將所有的波參數記錄在一個vector中。
#pragma once
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <vector>
const float PI = 3.1415926f;
const float RAD2ANGLE = 180 / PI;
const int TBL_SIZE_MUL = 1;
const int TBL_SIZE = 360 * TBL_SIZE_MUL;
class cRipple
{
public:
enum E_LIGHTSOURCE
{
E_LIGHTSOURCE_SELF_PARALLEL_BOTTOM,//圖像在水底自發光,光平行從水底向水面折射
E_LIGHTSOURCE_SELF_PARALLEL_TOP,//圖像在水面自發光,光平行從水面反射
E_LIGHTSOURCE_OTHER_PARALLEL_TOP,//圖像在水底不發光,水面平行光源照射水底圖像
};
enum E_RIPPLE_TYPE
{
E_RIPPLE_PARALLEL,//平行波
E_RIPPLE_CIRCLE,//環形波
};
structs RippleInfo
{
E_LIGHTSOURCE light;
E_RIPPLE_TYPE type;
float A;//振幅
float w;//角速度
float v;//波速
float dir;//平行波的方向
float t_begin;//起始時間
cv::Point2f O;//環形波的圓心
sRippleInfo(float A, float w, float v, cv::Point2f O );
sRippleInfo(float A, float w, float v, float dir );
};
cRipple(float refractivity, float cost_t, float cost_dist );
~cRipple();
void InitRefractionTbl( float n );
void AddRipple( const sRippleInfo&info);
float GetRefraction( float dist );
void CircleRipple( const sRippleInfo&info, float time );
void ParallelRipple( const sRippleInfo&info, float time );
voidProcess( cv::Mat&src, cv::Mat&dst );
private:
std::vector<sRippleInfo> _ripples;
float _refractivity;//折射率
float _cost_dist;//距離衰減係數
float _cost_t;//時間衰減係數
cv::Mat _mapx, _mapy;//座標映射表
float _refractionTbl[TBL_SIZE], _reflectionTbl[TBL_SIZE];
};
對應的cpp程序
#include "cRipple.h"
using namespace std;
using namespace cv;
cRipple::cRipple( float refractivity, floatcost_t, float cost_dist )
:_refractivity(refractivity ), _cost_t( cost_t ), _cost_dist( cost_dist )
{
InitRefractionTbl(refractivity );
}
cRipple::~cRipple()
{
}
cRipple::sRippleInfo::sRippleInfo( float A,float w, float v, cv::Point2f O )
{
t_begin= static_cast<float>( getTickCount() / getTickFrequency() );
this->A= A;
this->w= w;
this->v= v;
this->light= E_LIGHTSOURCE::E_LIGHTSOURCE_SELF_PARALLEL_TOP;
this->type= E_RIPPLE_TYPE::E_RIPPLE_CIRCLE;
this->O= O;
}
cRipple::sRippleInfo::sRippleInfo( float A,float w, float v, float dir )
{
t_begin= static_cast<float>( getTickCount() / getTickFrequency() );
this->A= A;
this->w= w;
this->v= v;
this->light= E_LIGHTSOURCE::E_LIGHTSOURCE_SELF_PARALLEL_TOP;
this->type= E_RIPPLE_TYPE::E_RIPPLE_PARALLEL;
this->dir= dir;
}
void cRipple::InitRefractionTbl( float n )
{
static float ln = 0;
if( n != ln )//如果新的n不同則更新折射表,不考慮比較過程中的浮點誤差
{
for( auto i = 0u; i < sizeof( _refractionTbl ) / sizeof( _refractionTbl[0] );i++ )
{
float alpha = atanf( -sinf( 2 * i * PI / ( sizeof( _refractionTbl ) / sizeof(_refractionTbl[0] ) ) ) ); //切線與x軸夾角,相當於入射角
float gamma = asinf( sinf( alpha ) / n );//折射角
_refractionTbl[i]= tanf( alpha - gamma );//= y + A*tanf( 2 * alpha );//折射後偏差值
}
ln= n;
}
}
float cRipple::GetRefraction( float dist )
{
floatremain = fmodf( dist, 2 * PI );//用角度表示的行程,並取餘,應當爲從
if( remain < 0 )remain += 2 * PI;
elseremain = 0;//負數時刻不計算
return_refractionTbl[static_cast<int> ( remain / 2 / PI* TBL_SIZE )];//查表,向下取整,精度略有損失
}
void cRipple::AddRipple( constcRipple::sRippleInfo&info )
{
_ripples.push_back(info );
}
void cRipple::Process( cv::Mat&src,cv::Mat&dst )
{
InitRefractionTbl(_refractivity );
if( _ripples.size() == 0 )
{
dst= src.clone();
return;
}
//初始化映射表
_mapx.create(src.rows, src.cols, CV_32FC1);
_mapy.create(src.rows, src.cols, CV_32FC1);
for( int x = 0; x < src.cols; x++ )
_mapx.col(x ) = x;
for( int y = 0; y < src.rows; y++ )
_mapy.row(y ) = y;
double time = getTickCount() / getTickFrequency();
//疊加波形
for( auto it = _ripples.cbegin(); it != _ripples.cend(); )
{
float period = time - it->t_begin;
if( expf( -_cost_t*( period ) ) > 0.01f )//太小則刪除
{
switch( it->type )
{
case E_RIPPLE_TYPE::E_RIPPLE_CIRCLE:
CircleRipple(*it, period );
break;
case E_RIPPLE_TYPE::E_RIPPLE_PARALLEL:
ParallelRipple(*it, period );
break;
default:
break;
}
++it;
}
else
it = _ripples.erase( it );
}
remap(src, dst, _mapx, _mapy, INTER_LINEAR, cv::BORDER_REPLICATE );//DEBUG下大約3ms數量級
}
void cRipple::CircleRipple( constsRippleInfo&info, float time )
{
float*px = (float*)_mapx.data, *py = (float*)_mapy.data;
float cost_time = expf( -_cost_t * time );
float radius = ::logf( 1.0f / 0.01f/cost_time )/_cost_dist;
if( radius > info.v*time )
radius= 10*info.v*time;
Rect region( info.O.x - radius, info.O.y - radius, 2 * radius, 2 * radius );
if(region.x<0 ) region.x = 0;
if( region.y < 0 ) region.y = 0;
if( region.x + region.width > _mapx.cols )region.width = _mapx.cols -region.x;
if( region.y + region.height > _mapx.rows )region.height = _mapx.rows -region.y;
//#pragma omp parallel for
for( int y = region.y; y < region.y+region.height; y++ )
{
px= _mapx.ptr<float>( y , region.x );
py= _mapy.ptr<float>( y , region.x );
for( int x = region.x; x < region.x+region.width; x++ )
{
floatdist = sqrtf( ( x - info.O.x )*( x - info.O.x ) + ( y - info.O.y )*( y -info.O.y ) );//點與波紋方向直線的距離
floatdelta = info.A* GetRefraction( info.w*dist - info.v*time );
floate_cost = cost_time*::expf( -_cost_dist*dist );//隨距離衰減係數
*px+++= e_cost*delta* ( x - info.O.x ) /dist;
*py+++= e_cost*delta* ( y - info.O.y ) /dist;
}
}
}
void cRipple::ParallelRipple( constsRippleInfo&info, float time )
{
floatremain = fmodf( info.dir, 2 * PI / 4 );//角度化爲0~2π內的角度
if( remain < 0 )remain += 2 * PI / 4;//防止負數
floatcosrad = cosf( info.dir ), sinrad = sinf( info.dir );
if( remain < 0.1 || remain>4 - 0.1 )//水平或垂直5度以內
{
for( int x = 0; x < _mapx.cols; x++ )
_mapx.col(x ) = x + info.A*GetRefraction( ( info.w*x - info.v*time )*cosrad );
for( int y = 0; y < _mapx.rows; y++ )
_mapy.row(y ) = y + info.A*GetRefraction( ( info.w*y - info.v*time )*sinrad );
}
else
{
//過零點的波紋直線爲-kx+y=0,距離爲
floatk = tanf( info.dir ), divk = sqrtf( 1 + k*k );
float*px = (float*)_mapx.data, *py = (float*)_mapy.data;
for( int y = 0; y < _mapx.rows; y++ )
{
for( int x = 0; x < _mapx.cols; x++ )
{
//查表法,效率高的方法
floatdist = -( -k*x + y ) / divk;//點與波紋方向直線的距離
floatdelta = info.A* GetRefraction( info.w*dist - info.v*time );
*px+++= delta*cosrad;
*py+++= delta*sinrad;
}
}
}
}
進化:損耗模型
進化:渾濁度模型
優化:多線程
進化:反射模型
優化:GPU加速