【OpenCV入門教程之十七】OpenCV重對映 SURF特徵點檢測合輯
本篇文章中,我們一起探討了OpenCV中重對映和SURF特徵點檢測相關的知識點,主要一起了解OpenCV中重對映相關的函式remap,SURF演算法在OpenCV中的體現與應用。此博文一共有三個配套的麻雀雖小但五臟俱全的示例程式,其經過淺墨詳細註釋過的程式碼都在文中貼出,且文章最後提供了綜合示例程式的下載。
依然是先看看程式執行截圖。
重對映:
SURF特徵點檢測:
一、OpenCV重對映
1.1 重對映的概念簡析
重對映,就是把一幅影象中某位置的畫素放置到另一個圖片指定位置的過程。為了完成對映過程, 我們需要獲得一些插值為非整數畫素的座標,因為源影象與目標影象的畫素座標不是一一對應的。一般情況下,我們通過重對映來表達每個畫素的位置 (x,y),像這樣 :
g(x,y) = f ( h(x,y) )
在這裡, g( ) 是目標影象, f() 是源影象, 而h(x,y) 是作用於 (x,y) 的對映方法函式。
來看個例子。 若有一幅影象 I ,想滿足下面的條件作重對映:
h(x,y) = (I.cols - x, y )
這樣的話,影象會按照 x 軸方向發生翻轉。那麼,源影象和效果圖分別如下:
在OpenCV中,我們用函式remap( )來實現簡單重對映,下面我們就一起來看看這個函式。
1.2 remap( )函式解析
remap( )函式會根據我們指定的對映形式,將源影象進行重對映幾何變換,基於的式子如下:
需要注意,此函式不支援就地(in-place)操作。看看其原型和引數。
C++: void remap(InputArray src, OutputArraydst, InputArray map1, InputArray map2, int interpolation, intborderMode=BORDER_CONSTANT, const Scalar& borderValue=Scalar())
- 第一個引數,InputArray型別的src,輸入影象,即源影象,填Mat類的物件即可,且需為單通道8位或者浮點型影象。
- 第二個引數,OutputArray型別的dst,函式呼叫後的運算結果存在這裡,即這個引數用於存放函式呼叫後的輸出結果,需和源圖片有一樣的尺寸和型別。
- 第三個引數,InputArray型別的map1,它有兩種可能的表示物件。
- 表示點(x,y)的第一個對映。
- 表示CV_16SC2 , CV_32FC1 或CV_32FC2型別的X值。
- 第四個引數,InputArray型別的map2,同樣,它也有兩種可能的表示物件,而且他是根據map1來確定表示那種物件。
- 若map1表示點(x,y)時。這個引數不代表任何值。
- 表示CV_16UC1 , CV_32FC1型別的Y值(第二個值)。
- 第五個引數,int型別的interpolation,插值方式,之前的resize( )函式中有講到,需要注意,resize( )函式中提到的INTER_AREA插值方式在這裡是不支援的,所以可選的插值方式如下:
- INTER_NEAREST - 最近鄰插值
- INTER_LINEAR – 雙線性插值(預設值)
- INTER_CUBIC – 雙三次樣條插值(逾4×4畫素鄰域內的雙三次插值)
- INTER_LANCZOS4 -Lanczos插值(逾8×8畫素鄰域的Lanczos插值)
- 第六個引數,int型別的borderMode,邊界模式,有預設值BORDER_CONSTANT,表示目標影象中“離群點(outliers)”的畫素值不會被此函式修改。
- 第七個引數,const Scalar&型別的borderValue,當有常數邊界時使用的值,其有預設值Scalar( ),即預設值為0。
1.3 詳細註釋的重對映示例程式
下面放出精簡後的以remap函式為核心的示例程式,方便大家快速掌握remap函式的使用方法。
//-----------------------------------【程式說明】----------------------------------------------// 程式名稱::《【OpenCV入門教程之十七】OpenCV重對映 & SURF特徵點檢測合輯 》 博文配套原始碼 // 開發所用IDE版本:Visual Studio 2010// 開發所用OpenCV版本: 2.4.9// 2014年5月26日 Created by 淺墨// 配套博文連結: http://blog.csdn.net/poem_qianmo/article/details/26977557// PS:程式結合配合博文學習效果更佳// 淺墨的微博:@淺墨_毛星雲 http://weibo.com/1723155442// 淺墨的知乎:http://www.zhihu.com/people/mao-xing-yun// 淺墨的豆瓣:http://www.douban.com/people/53426472///----------------------------------------------------------------------------------------------//-----------------------------------【標頭檔案包含部分】---------------------------------------// 描述:包含程式所依賴的標頭檔案//---------------------------------------------------------------------------------------------- #include "opencv2/highgui/highgui.hpp"#include "opencv2/imgproc/imgproc.hpp"#include <iostream>//-----------------------------------【名稱空間宣告部分】--------------------------------------// 描述:包含程式所使用的名稱空間//-----------------------------------------------------------------------------------------------using namespace cv;//-----------------------------------【main( )函式】--------------------------------------------// 描述:控制檯應用程式的入口函式,我們的程式從這裡開始執行//-----------------------------------------------------------------------------------------------int main( ){ //【0】變數定義 Mat srcImage, dstImage; Mat map_x, map_y; //【1】載入原始圖 srcImage = imread( "1.jpg", 1 ); if(!srcImage.data ) { printf("讀取圖片錯誤,請確定目錄下是否有imread函式指定的圖片存在~! \n"); return false; } imshow("原始圖",srcImage); //【2】建立和原始圖一樣的效果圖,x重對映圖,y重對映圖 dstImage.create( srcImage.size(), srcImage.type() ); map_x.create( srcImage.size(), CV_32FC1 ); map_y.create( srcImage.size(), CV_32FC1 ); //【3】雙層迴圈,遍歷每一個畫素點,改變map_x & map_y的值 for( int j = 0; j < srcImage.rows;j++) { for( int i = 0; i < srcImage.cols;i++) { //改變map_x & map_y的值. map_x.at<float>(j,i) = static_cast<float>(i); map_y.at<float>(j,i) = static_cast<float>(srcImage.rows - j); } } //【4】進行重對映操作 remap( srcImage, dstImage, map_x, map_y, CV_INTER_LINEAR, BORDER_CONSTANT, Scalar(0,0, 0) ); //【5】顯示效果圖 imshow( "【程式視窗】", dstImage ); waitKey(); return 0;}
顯示效果圖:
最近世界盃正如火如荼地進行著,這裡的圖片素材就是巴西隊的球星們~
1.4 OpenCV2.X中remap函式原始碼
這裡我們放出remap函式的原始碼,供需要了解其實現細節的朋友們觀看,淺墨在這裡不花時間對其進行剖析。
void cv::remap( InputArray _src, OutputArray _dst, InputArray _map1, InputArray _map2, int interpolation, int borderType, const Scalar& borderValue ){ static RemapNNFunc nn_tab[] = { remapNearest<uchar>, remapNearest<schar>, remapNearest<ushort>, remapNearest<short>, remapNearest<int>, remapNearest<float>, remapNearest<double>, 0 }; static RemapFunc linear_tab[] = { remapBilinear<FixedPtCast<int, uchar, INTER_REMAP_COEF_BITS>, RemapVec_8u, short>, 0, remapBilinear<Cast<float, ushort>, RemapNoVec, float>, remapBilinear<Cast<float, short>, RemapNoVec, float>, 0, remapBilinear<Cast<float, float>, RemapNoVec, float>, remapBilinear<Cast<double, double>, RemapNoVec, float>, 0 }; static RemapFunc cubic_tab[] = { remapBicubic<FixedPtCast<int, uchar, INTER_REMAP_COEF_BITS>, short, INTER_REMAP_COEF_SCALE>, 0, remapBicubic<Cast<float, ushort>, float, 1>, remapBicubic<Cast<float, short>, float, 1>, 0, remapBicubic<Cast<float, float>, float, 1>, remapBicubic<Cast<double, double>, float, 1>, 0 }; static RemapFunc lanczos4_tab[] = { remapLanczos4<FixedPtCast<int, uchar, INTER_REMAP_COEF_BITS>, short, INTER_REMAP_COEF_SCALE>, 0, remapLanczos4<Cast<float, ushort>, float, 1>, remapLanczos4<Cast<float, short>, float, 1>, 0, remapLanczos4<Cast<float, float>, float, 1>, remapLanczos4<Cast<double, double>, float, 1>, 0 }; Mat src = _src.getMat(), map1 = _map1.getMat(), map2 = _map2.getMat(); CV_Assert( map1.size().area() > 0 ); CV_Assert( !map2.data || (map2.size() == map1.size())); _dst.create( map1.size(), src.type() ); Mat dst = _dst.getMat(); if( dst.data == src.data ) src = src.clone(); int depth = src.depth(); RemapNNFunc nnfunc = 0; RemapFunc ifunc = 0; const void* ctab = 0; bool fixpt = depth == CV_8U; bool planar_input = false; if( interpolation == INTER_NEAREST ) { nnfunc = nn_tab[depth]; CV_Assert( nnfunc != 0 ); } else { if( interpolation == INTER_AREA ) interpolation = INTER_LINEAR; if( interpolation == INTER_LINEAR ) ifunc = linear_tab[depth]; else if( interpolation == INTER_CUBIC ) ifunc = cubic_tab[depth]; else if( interpolation == INTER_LANCZOS4 ) ifunc = lanczos4_tab[depth]; else CV_Error( CV_StsBadArg, "Unknown interpolation method" ); CV_Assert( ifunc != 0 ); ctab = initInterTab2D( interpolation, fixpt ); } const Mat *m1 = &map1, *m2 = &map2; if( (map1.type() == CV_16SC2 && (map2.type() == CV_16UC1 || map2.type() == CV_16SC1 || !map2.data)) || (map2.type() == CV_16SC2 && (map1.type() == CV_16UC1 || map1.type() == CV_16SC1 || !map1.data)) ) { if( map1.type() != CV_16SC2 ) std::swap(m1, m2); } else { CV_Assert( ((map1.type() == CV_32FC2 || map1.type() == CV_16SC2) && !map2.data) || (map1.type() == CV_32FC1 && map2.type() == CV_32FC1) ); planar_input = map1.channels() == 1; } RemapInvoker invoker(src, dst, m1, m2, interpolation, borderType, borderValue, planar_input, nnfunc, ifunc, ctab); parallel_for_(Range(0, dst.rows), invoker, dst.total()/(double)(1<<16));}
好了,重對映先就講這麼多,在文章末尾還有一個綜合一點的示例程式供大家學習。下面我們開始講解SURF相關的內容。
二.SURF特徵點檢測
SURF演算法有一些不錯的內容和用法,OpenCV中使用頗多,淺墨會花一些篇幅對其進行講解。今天的這篇文章只是一個小小的開頭,主要介紹SURF特徵點檢測。
先簡單瞭解一下SURF演算法的大概內容吧。
2.1 SURF演算法概覽
SURF,我們簡單介紹一下,英語全稱為SpeededUp Robust Features,直譯的話就是“加速版的具有魯棒性的特徵“演算法,由Bay在2006年首次提出。SURF是尺度不變特徵變換演算法(SIFT演算法)的加速版。一般來說,標準的SURF運算元比SIFT運算元快好幾倍,並且在多幅圖片下具有更好的穩定性。SURF最大的特徵在於採用了harr特徵以及積分影象的概念,這大大加快了程式的執行時間。SURF可以應用於計算機視覺的物體識別以及3D重構中。
PS: 由於我們的專欄側重點是教大家如何快速入門OpenCV程式設計,不是來進行影象處理科普的,所以原理部分不會花筆墨多講。一方面是淺墨也不喜歡講這些枯燥的概念,另一方面是大家肯定應該也不喜歡看這些枯燥的原理,大家是喜歡看程式碼的〜( ̄▽ ̄〜)。就像小魏CPU童鞋在部落格上寫的,“Talk is cheap. Show me thecode.”
所以原理部分大家就自行用搜索引擎去學習吧,淺墨會將更多的筆墨用來分享網路上獨一無二的乾貨。
2.2 前世今生——SURF類相關OpenCV原始碼剖析
OpenCV中關於SURF演算法的部分,常常涉及到的是SURF、SurfFeatureDetector、SurfDescriptorExtractor這三個類,這一小節我們就來對他們進行人肉,挖挖其背景,看看他們究竟是什麼來頭。
在D:\Program Files (x86)\opencv\sources\modules\nonfree\include\opencv2\nonfree下的features2d.hpp標頭檔案中,我們可以發現這樣兩句定義:
typedef SURF SurfFeatureDetector;typedef SURF SurfDescriptorExtractor;
我們都知道,typedef宣告是為現有型別建立一個新的名字,類型別名。這就表示,SURF類忽然同時有了兩個新名字SurfFeatureDetector以及SurfDescriptorExtractor。
也就是說,我們平常使用的SurfFeatureDetector類和SurfDescriptorExtractor類,其實就是SURF類,他們三者等價。
然後在這兩句定義的上方,我們可以看到SURF類的類宣告全貌:
class CV_EXPORTS_W SURF : public Feature2D{public: //! the default constructor CV_WRAP SURF(); //! the full constructor taking all the necessary parameters explicit CV_WRAP SURF(double hessianThreshold, int nOctaves=4, int nOctaveLayers=2, bool extended=true, bool upright=false); //! returns the descriptor size in float's (64 or 128) CV_WRAP int descriptorSize() const; //! returns the descriptor type CV_WRAP int descriptorType() const; //! finds the keypoints using fast hessian detector used in SURF void operator()(InputArray img, InputArray mask, CV_OUT vector<KeyPoint>& keypoints) const; //! finds the keypoints and computes their descriptors. Optionally it can compute descriptors for the user-provided keypoints void operator()(InputArray img, InputArray mask, CV_OUT vector<KeyPoint>& keypoints, OutputArray descriptors, bool useProvidedKeypoints=false) const; AlgorithmInfo* info() const; CV_PROP_RW double hessianThreshold; CV_PROP_RW int nOctaves; CV_PROP_RW int nOctaveLayers; CV_PROP_RW bool extended; CV_PROP_RW bool upright;protected: void detectImpl( const Mat& image, vector<KeyPoint>& keypoints, const Mat& mask=Mat() ) const; void computeImpl( const Mat& image, vector<KeyPoint>& keypoints, Mat& descriptors ) const;};
可以觀察到,SURF類公共繼承自Feature2D類,我們再次進行轉到,可以在路徑d:\Program Files(x86)\opencv\build\include\opencv2\features2d\features2d.hpp看到Feature2D類的宣告:
class CV_EXPORTS_W Feature2D : public FeatureDetector, public DescriptorExtractor{public: /* * Detect keypoints in an image. * image The image. * keypoints The detected keypoints. * mask Mask specifying where to look for keypoints (optional). Must be a char * matrix with non-zero values in the region of interest. * useProvidedKeypoints If true, the method will skip the detection phase and will compute * descriptors for the provided keypoints */ CV_WRAP_AS(detectAndCompute) virtual void operator()( InputArray image, InputArray mask, CV_OUT vector<KeyPoint>& keypoints, OutputArray descriptors, bool useProvidedKeypoints=false ) const = 0; CV_WRAP void compute( const Mat& image, CV_OUT CV_IN_OUT std::vector<KeyPoint>& keypoints, CV_OUT Mat& descriptors ) const; // Create feature detector and descriptor extractor by name. CV_WRAP static Ptr<Feature2D> create( const string& name );};
顯然,Feature2D類又是公共繼承自FeatureDetector以及 DescriptorExtractor類。繼續刨根問底,我們看看其父類FeatureDetector以及 DescriptorExtractor類的定義。
首先是FeatureDetector類:
/************************************ Base Classes ************************************//* * Abstract base class for 2D image feature detectors. */class CV_EXPORTS_W FeatureDetector : public virtual Algorithm{public: virtual ~FeatureDetector(); /* * Detect keypoints in an image. * image The image. * keypoints The detected keypoints. * mask Mask specifying where to look for keypoints (optional). Must be a char * matrix with non-zero values in the region of interest. */ CV_WRAP void detect( const Mat& image, CV_OUT vector<KeyPoint>& keypoints, const Mat& mask=Mat() ) const; /* * Detect keypoints in an image set. * images Image collection. * keypoints Collection of keypoints detected in an input images. keypoints[i] is a set of keypoints detected in an images[i]. * masks Masks for image set. masks[i] is a mask for images[i]. */ void detect( const vector<Mat>& images, vector<vector<KeyPoint> >& keypoints, const vector<Mat>& masks=vector<Mat>() ) const; // Return true if detector object is empty CV_WRAP virtual bool empty() const; // Create feature detector by detector name. CV_WRAP static Ptr<FeatureDetector> create( const string& detectorType );protected: virtual void detectImpl( const Mat& image, vector<KeyPoint>& keypoints, const Mat& mask=Mat() ) const = 0; /* * Remove keypoints that are not in the mask. * Helper function, useful when wrapping a library call for keypoint detection that * does not support a mask argument. */ static void removeInvalidPoints( const Mat& mask, vector<KeyPoint>& keypoints );};
這裡,我們看到了我們以後經常會用到的detect( )方法過載的兩個原型,原來是SURF類經過兩層的繼承,從FeatureDetector類繼承而來的。
/* * Detect keypoints in an image. * image The image. * keypoints The detected keypoints. * mask Mask specifying where to look for keypoints (optional). Must be a char * matrix with non-zero values in the region of interest. */ CV_WRAP void detect( const Mat& image, CV_OUT vector<KeyPoint>& keypoints, const Mat& mask=Mat() ) const; /* * Detect keypoints in an image set. * images Image collection. * keypoints Collection of keypoints detected in an input images. keypoints[i] is a set of keypoints detected in an images[i]. * masks Masks for image set. masks[i] is a mask for images[i]. */ void detect( const vector<Mat>& images, vector<vector<KeyPoint> >& keypoints, const vector<Mat>& masks=vector<Mat>() ) const;
同樣,看看SURF類的另一個“爺爺”DescriptorExtractor類的宣告。
/* * Abstract base class for computing descriptors for image keypoints. * * In this interface we assume a keypoint descriptor can be represented as a * dense, fixed-dimensional vector of some basic type. Most descriptors used * in practice follow this pattern, as it makes it very easy to compute * distances between descriptors. Therefore we represent a collection of * descriptors as a Mat, where each row is one keypoint descriptor. */class CV_EXPORTS_W DescriptorExtractor : public virtual Algorithm{public: virtual ~DescriptorExtractor(); /* * Compute the descriptors for a set of keypoints in an image. * image The image. * keypoints The input keypoints. Keypoints for which a descriptor cannot be computed are removed. * descriptors Copmputed descriptors. Row i is the descriptor for keypoint i. */ CV_WRAP void compute( const Mat& image, CV_OUT CV_IN_OUT vector<KeyPoint>& keypoints, CV_OUT Mat& descriptors ) const; /* * Compute the descriptors for a keypoints collection detected in image collection. * images Image collection. * keypoints Input keypoints collection. keypoints[i] is keypoints detected in images[i]. * Keypoints for which a descriptor cannot be computed are removed. * descriptors Descriptor collection. descriptors[i] are descriptors computed for set keypoints[i]. */ void compute( const vector<Mat>& images, vector<vector<KeyPoint> >& keypoints, vector<Mat>& descriptors ) const; CV_WRAP virtual int descriptorSize() const = 0; CV_WRAP virtual int descriptorType() const = 0; CV_WRAP virtual bool empty() const; CV_WRAP static Ptr<DescriptorExtractor> create( const string& descriptorExtractorType );protected: virtual void computeImpl( const Mat& image, vector<KeyPoint>& keypoints, Mat& descriptors ) const = 0; /* * Remove keypoints within borderPixels of an image edge. */ static void removeBorderKeypoints( vector<KeyPoint>& keypoints, Size imageSize, int borderSize );};
上述程式碼表明FeatureDetector 類和DescriptorExtractor類都虛繼承自Algorithm基類。
呼,歷經千辛萬苦,終於,我們找到SURF類德高望重的祖先——OpenCV中的Algorithm基類。看看其原型宣告:
/*! Base class for high-level OpenCV algorithms*/class CV_EXPORTS_W Algorithm{public: Algorithm(); virtual ~Algorithm(); string name() const; template<typename _Tp> typename ParamType<_Tp>::member_type get(const string& name) const; template<typename _Tp> typename ParamType<_Tp>::member_type get(const char* name) const; CV_WRAP int getInt(const string& name) const; CV_WRAP double getDouble(const string& name) const; CV_WRAP bool getBool(const string& name) const; CV_WRAP string getString(const string& name) const; CV_WRAP Mat getMat(const string& name) const; CV_WRAP vector<Mat> getMatVector(const string& name) const; CV_WRAP Ptr<Algorithm> getAlgorithm(const string& name) const; void set(const string& name, int value); void set(const string& name, double value); void set(const string& name, bool value); void set(const string& name, const string& value); void set(const string& name, const Mat& value); void set(const string& name, const vector<Mat>& value); void set(const string& name, const Ptr<Algorithm>& value); template<typename _Tp> void set(const string& name, const Ptr<_Tp>& value); CV_WRAP void setInt(const string& name, int value); CV_WRAP void setDouble(const string& name, double value); CV_WRAP void setBool(const string& name, bool value); CV_WRAP void setString(const string& name, const string& value); CV_WRAP void setMat(const string& name, const Mat& value); CV_WRAP void setMatVector(const string& name, const vector<Mat>& value); CV_WRAP void setAlgorithm(const string& name, const Ptr<Algorithm>& value); template<typename _Tp> void setAlgorithm(const string& name, const Ptr<_Tp>& value); void set(const char* name, int value); void set(const char* name, double value); void set(const char* name, bool value); void set(const char* name, const string& value); void set(const char* name, const Mat& value); void set(const char* name, const vector<Mat>& value); void set(const char* name, const Ptr<Algorithm>& value); template<typename _Tp> void set(const char* name, const Ptr<_Tp>& value); void setInt(const char* name, int value); void setDouble(const char* name, double value); void setBool(const char* name, bool value); void setString(const char* name, const string& value); void setMat(const char* name, const Mat& value); void setMatVector(const char* name, const vector<Mat>& value); void setAlgorithm(const char* name, const Ptr<Algorithm>& value); template<typename _Tp> void setAlgorithm(const char* name, const Ptr<_Tp>& value); CV_WRAP string paramHelp(const string& name) const; int paramType(const char* name) const; CV_WRAP int paramType(const string& name) const; CV_WRAP void getParams(CV_OUT vector<string>& names) const; virtual void write(FileStorage& fs) const; virtual void read(const FileNode& fn); typedef Algorithm* (*Constructor)(void); typedef int (Algorithm::*Getter)() const; typedef void (Algorithm::*Setter)(int); CV_WRAP static void getList(CV_OUT vector<string>& algorithms); CV_WRAP static Ptr<Algorithm> _create(const string& name); template<typename _Tp> static Ptr<_Tp> create(const string& name); virtual AlgorithmInfo* info() const /* TODO: make it = 0;*/ { return 0; }};
關於這幾個類纏綿悱惻的關係,畫個圖就一目瞭然了,也就是這樣的過程:
3.3 drawKeypoints函式詳解
因為接下來的示例程式需要用到drawKeypoints函式,我們在這裡順便講一講。
顧名思義,此函式用於繪製關鍵點。
C++: void drawKeypoints(const Mat&image, const vector<KeyPoint>& keypoints, Mat& outImage, constScalar& color=Scalar::all(-1), int flags=DrawMatchesFlags::DEFAULT )
- 第一個引數,const Mat&型別的src,輸入影象。
- 第二個引數,const vector<KeyPoint>&型別的keypoints,根據源影象得到的特徵點,它是一個輸出引數。
- 第三個引數,Mat&型別的outImage,輸出影象,其內容取決於第五個引數識別符號falgs。
- 第四個引數,const Scalar&型別的color,關鍵點的顏色,有預設值Scalar::all(-1)。
- 第五個引數,int型別的flags,繪製關鍵點的特徵識別符號,有預設值DrawMatchesFlags::DEFAULT。可以在如下這個結構體中選取值。
struct DrawMatchesFlags{ enum { DEFAULT = 0, // Output image matrix will be created (Mat::create), // i.e. existing memory of output image may be reused. // Two source images, matches, and single keypoints // will be drawn. // For each keypoint, only the center point will be // drawn (without a circle around the keypoint with the // keypoint size and orientation). DRAW_OVER_OUTIMG = 1, // Output image matrix will not be // created (using Mat::create). Matches will be drawn // on existing content of output image. NOT_DRAW_SINGLE_POINTS = 2, // Single keypoints will not be drawn. DRAW_RICH_KEYPOINTS = 4 // For each keypoint, the circle around // keypoint with keypoint size and orientation will // be drawn. };};
三、綜合示例部分
因為這次的兩個知識點關聯度不大,所以不方便組織起來成為一個綜合示例程式。在這裡我們分開將其放出。
3.1 重對映綜合示例程式
先放出以remap為核心的綜合示例程式,可以用按鍵控制四種不同的對映模式。且利用了OpenCV版本標識巨集“CV_VERSION”,在幫助文字相關程式碼中加入了一句