這次介紹opencv中一個簡單的點運算函數,用來實現圖片合成。 對應於例程中的 (TUTORIAL) AddingImages 和 (TUTORIAL) AddingImagesTrackbar。
Opencv中提供的函數是addWeighted,用法很簡單,爲了比較性能了,我們手工實現了一段相同功能的代碼,並比較兩者的性能,到底快了多少,且看下文分解。
圖像處理中一個簡單而有趣的點運算操作可以用以下的公式表示,可以實現兩張圖片的線性融合。
這裏α 表示兩種圖片的融合比例,這個g(x) 表示 融合圖片中的像素點,f0(x) 和 f1(x) 分別表示背景和前景圖片中的像素點。
下面爲例程中的函數調用,
beta = ( 1.0 - alpha );
addWeighted( src1, alpha, src2, beta, 0.0, dst);
opencv 通過 addWeighted 函數實現圖片的線性融合,這個函數在之前的例程中也有提到過。
這個函數的原型如下所示,可以看出這個函數最小需要6個參數。
1、 第1個參數,輸入圖片1,
2、第2個參數,圖片1的融合比例
3、第3個參數,輸入圖片2
4、第4個參數,圖片2的融合比例
5、第5個參數,偏差
6、第6個參數,輸出圖片
//! computes weighted sum of two arrays (dst = alpha*src1 + beta*src2 + gamma)
CV_EXPORTS_W void addWeighted(InputArray src1, double alpha, InputArray src2,
double beta, double gamma, OutputArray dst, int dtype=-1);
下面是程序運行結果
程序會提示輸入第一個圖片的融合比例,輸入0.5時,圖片的效果。
不過有時候會想,調節alpha 的值,查看不同情況下的融合結果。這個也不難做到,在例程(TUTORIAL) AddingImagesTrackbar中已經幫我實現了
先看看運行的結果。
相比之前的結果,可以看出在圖像上方多了一個滑動條,用來改變alpha的值,範圍從0~100。
相應的代碼如下:
/// Create Windows
namedWindow("Linear Blend", 1);
/// Create Trackbars
char TrackbarName[50];
sprintf( TrackbarName, "Alpha x %d", alpha_slider_max );
createTrackbar( TrackbarName, "Linear Blend", &alpha_slider, alpha_slider_max, on_trackbar );
/// Show some stuff
on_trackbar( alpha_slider, 0 );
利用highgui庫中的 createTrackbar 函數創建一個滑塊,函數的原型如下
CV_EXPORTS int createTrackbar(const string& trackbarname, const string& winname,
int* value, int count,
TrackbarCallback onChange = 0,
void* userdata = 0);
可以看到,這個函數有6個參數,最後一個爲用戶數據的指針,類似回調函數中的用戶數據指針。
1、第一個參數,爲滑塊的名字
2、第二個參數,爲滑塊所在窗口的名字,用來獲取窗口的handle
3、第三個參數,爲傳人一個數據的指針,通過滑塊來改變該數據的值,該變量需要在回調函數TrackbarCallback 中作用域可見,因此例程中將它定義爲全局變量
4、第四個參數,爲傳人數據的最大值,用來控制數據的變化範圍
5、第五個參數,回調函數的函數指針,當滑塊變化時,便調用回調函數,實現融合畫面隨着滑塊的滑動而變化
下面我們來看下回調函數 on_trackbar, 它被定義了成一個static函數,意味着函數生命週期從被調用開始一直存在知道程序結束,第二個參數對應createTrackbar函數中的第6個參數,用戶數據指針。
/**
* @function on_trackbar
* @brief Callback for trackbar
*/
static void on_trackbar( int, void* )
{
alpha = (double) alpha_slider/alpha_slider_max ;
beta = ( 1.0 - alpha );
addWeighted( src1, alpha, src2, beta, 0.0, dst);
imshow( "Linear Blend", dst );
}
爲了測試addWeighted 函數性能,我們手工打造了一段相同功能的函數MyaddWeighted,代碼如下
void MyaddWeight(InputArray _src1, double alpha, InputArray _src2, double beta, double gamma, OutputArray _dst)
{
CV_Assert(_src1.depth() == CV_8U && _src2.depth() == CV_8U);
CV_Assert(_src1.channels() == _src2.channels() &&_src1.size() == _src2.size());
_dst.create(_src1.size(),_src1.type());
Mat src1 = _src1.getMat();
Mat src2 = _src2.getMat();
Mat dst = _dst.getMat();
int rows = src1.rows;
int cols = src1.cols * src1.channels();
if (src1.isContinuous() && src2.isContinuous() && dst.isContinuous())
{
cols *= rows;
rows = 1;
}
uchar* pSrc1 = NULL;
uchar* pSrc2 = NULL;
uchar* pDst = NULL;
for (int i=0; i<rows; i++)
{
pSrc1 = src1.ptr<uchar>(i);
pSrc2 = src2.ptr<uchar>(i);
pDst = dst.ptr<uchar>(i);
for (int j=0; j<cols; j++)
{
pDst[j] = saturate_cast<uchar>(pSrc1[j]*alpha + pSrc2[j]*beta + gamma);
}
}
}
在手工打造的程序中,運用C [ ]下標對圖像像素點進行遍歷,在前面的例程有過相關介紹,C[ ] 是三種遍歷方法中性能最好的。
下圖是各自方法 運行1000次的平均時間,可以看出手工打造的代碼,雖然很好理解,但相對於opencv 的addWeightd函數性能差了很多,運行時間是6:1 。
注意到在我們代碼中有用到 saturate_cast<uchar> 進行類型轉換以保證值的安全,那麼這種類型轉換方式對我們性能有多大的影響呢,我們通過去掉這條語句後,重新運行後的結果爲
可以看出,平均運行時間從3.7毫秒縮短到了2.4毫秒,佔了運行時間的1/3。
現在換過一個方法來寫MyAddWeight 函數,還記得opencv中LUT函數中用到的NAryMatIterator,這次我們利用它來重寫MyAddWeight
void MyaddWeight2(InputArray _src1, double alpha, InputArray _src2, double beta, double gamma, OutputArray _dst)
{
CV_Assert(_src1.depth() == CV_8U && _src2.depth() == CV_8U);
CV_Assert(_src1.channels() == _src2.channels() &&_src1.size() == _src2.size());
_dst.create(_src1.size(),_src1.type());
Mat src1 = _src1.getMat();
Mat src2 = _src2.getMat();
Mat dst = _dst.getMat();
int cn = src1.channels();
AddFun fun = AddFunTable[src1.depth()]; // 通過數據類型,從函數指針數組中選擇相應的函數進行調用
const Mat* arrays[] = {&src1, &src2, &dst,0}; // 注意需要在最後加一個0,作爲指針數組結束的標誌,以確定數組中有效指針的個數
uchar* ptrs[3];
NAryMatIterator it(arrays, ptrs);
int len = (int)it.size;
for( size_t i = 0; i < it.nplanes; i++, ++it )
fun(ptrs[0], ptrs[1], ptrs[2], len, cn, alpha, beta, gamma);
}
AddFun 是我們定義的一個函數指針類型,利用它加上一個函數指針數組AddFunTable,我們可以很容易的擴展MyAddWeight的使用範圍,使它對CV_16S, CV_32F的數據類型也能適用
typedef void (*AddFun) (const uchar*, const uchar*, const uchar*, int, int, double, double, double);
static void ADD8u_(const uchar* src1, const uchar* src2, uchar* dst, int len, int cn, double aplha, double beta, double gamma)
{
for (int i=0; i<len*cn; i++)
dst[i] = src1[i]*aplha + src2[i]*beta + gamma;
}
這個是函數ADD8u_ 的定義,相應的,我們可以快速的寫出適合其他數據類型,如CV_8U,CV_16S,CV_32F的處理函數,
static AddFun AddFunTable[] =
{
(AddFun)ADD8u_, 0
};
這個是函數指針數組,目前只實現了對數據類型CV_8U的操作,這樣就可以根據數據的類型,調用相應的函數,而不必針對每一種數據類型寫一個函數,大大提高了代碼的重用率,也大大減少了以後代碼維護的工作量,是一個很值得學習的技巧。
下面看下,這種寫法是否對性能有所提高。
可以看到,運行時間從2.48 毫秒減少到了2.29毫秒,性能提高了大約 7.6%左右,相對於代碼重用方面,性能上面的提高不是很明顯。