仿射变换详解warpAffine
今天遇到一个问题是关于仿射变换的,但是由于没有将仿射变换的具体原理型明白,看别人的代码看的很费解,最后终于在师兄的帮助下将原理弄明白了,我觉得最重要的是理解仿射变换可以看成是几种简单变换的复合实现,
具体实现形式即将几种简单变换的变换矩阵M相乘,这样就很容易理解啦
?
定义:仿射变换的功能是从二维坐标到二维坐标之间的线性变换,且保持二维图形的“平直性”和“平行性”。仿射变换可以通过一系列的原子变换的复合来实现,包括平移,缩放,翻转,旋转和剪切。
这类变换可以用一个33的矩阵M来表示,其最后一行为(0,0,1)。该变换矩阵将原坐标为(x,y)变换为新坐标(x'',y''),
即
OpenCV中相应的函数是:
void?warpAffine(InputArray?src,OutputArray?dst,InputArray?M,Size?dsize,int?flags=INTER_LINEAR,int?borderMode=BORDER_CONSTANT,constScalar&?borderValue=Scalar())?
Parameters:
src?–inputimage.
dst?–outputimagethathasthesize?dsize?andthesametypeas?src?.
M?–??transformationmatrix,最重要的东东了,本文中着重讲M的构造
dsize?–sizeoftheoutputimage.ansformation(??).
borderMode?–pixelextrapolationmethod(see?borderInterpolate());when?borderMode=BORDER_TRANSPARENT?,itmeansthatthepixelsinthedestinationimagecorrespondingtothe“outliers”inthesourceimagearenotmodifiedbythefunction.
borderValue?–valueusedincaseofaconstantborder;bydefault,itis0.
?下面介绍一些典型的仿射变换:
(1)平移,将每一点移到到(x+t,y+t),变换矩阵为
(2)缩放变换?将每一点的横坐标放大或缩小sx倍,纵坐标放大(缩小)到sy倍,变换矩阵为
(3)旋转变换原点:目标图形围绕原点顺时针旋转Θ弧度,变换矩阵为
(4)旋转变换?:目标图形以(x,y)为轴心顺时针旋转θ弧度,变换矩阵为
?
相当于两次平移与一次原点旋转变换的复合,即先将轴心(x,y)移到到原点,然后做旋转变换,最后将图片的左上角置为图片的原点,即
有的人可能会说为什么这么复杂呢,那是因为在opencv的图像处理中,所有对图像的处理都是从原点进行的,而图像的原点默认为图像的左上角,而我们对图像作旋转处理时一般以图像的中点为轴心,因此就需要做如下处理
?如果你觉得这样很麻烦,可以使用opencv中自带的Mat?getRotationMatrix2D(Point2fcenter,doubleangle,doublescale)函数获得变换矩阵M,
center:旋转中心
angle:旋转弧度,一定要将角度转换成弧度
scale:缩放尺度
它得到的矩阵是:
?其中α=scalecos(angle),β=scale?sing(angle)?,(center.x,center.y)表示旋转轴心
但是不得不说opencv的文档以及相关书籍中都把这个矩阵写错了,如下:
建议大家自己通过下式验证一下,即首先将轴心(x,y)移到原点,然后做旋转平绽放变换,最后再将图像的左上角转换为原点
没有去研究该函数的源码,不晓得源码中到底怎么写的,但是在别人的博客中看到这个函数貌似需要修正
?
opencv中还有一个函数:Mat?getAffineTransform(InputArray?src,InputArray?dst)?
它通过三组点对就可以获得它们之间的仿射变换,如果我们在一组图像变换中知道变换后的三组点,那么我们就可以利用该函数求得变换矩阵,然后对整张图片进行仿射变换
还有一种与仿射变换经常混淆的变换为透视变换,透视变换需要四组点对才能确定变换矩阵,由于仿射变换保持“平直性”与“平行性”,因此只需要三组点对,而透视变换没有这种约束,故需要四组点对
?
warpPerspective函数
主要作用:对图像进行透视变换,就是变形
函数的调用形式:
C++:?void?warpPerspective(InputArray?src,OutputArray?dst,InputArray?M,Size?dsize,int?flags=INTER_LINEAR,int?borderMode=BORDER_CONSTANT,constScalar&?borderValue=Scalar())
参数详解:
InputArray?src:输入的图像
OutputArray?dst:输出的图像
InputArray?M:透视变换的矩阵
Size?dsize:输出图像的大小
int?flags=INTER_LINEAR:输出图像的插值方法,
combinationofinterpolationmethods(INTER_LINEAR?or?INTER_NEAREST)andtheoptionalflagWARP_INVERSE_MAP,thatsets?M?astheinversetransformation(??)
int?borderMode=BORDER_CONSTANT:图像边界的处理方式
constScalar&?borderValue=Scalar():边界的颜色设置,一般默认是0
函数原理:
透视变换(PerspectiveTransformation)是将图片投影到一个新的视平面(ViewingPlane),也称作投影映射(ProjectiveMapping)。通用的变换公式为:
u,v是原始图片左边,对应得到变换后的图片坐标x,y,其中。变换矩阵可以拆成4部分,表示线性变换,比如scaling,shearing和ratotion。用于平移,产生透视变换。所以可以理解成仿射等是透视变换的特殊形式。经过透视变换之后的图片通常不是平行四边形(除非映射视平面和原来平面平行的情况)。
重写之前的变换公式可以得到:
所以,已知变换对应的几个点就可以求取变换公式。反之,特定的变换公式也能新的变换后的图片。简单的看一个正方形到四边形的变换:变换的4组对应点可以表示成:
根据变换公式得到:
定义几个辅助变量:
都为0时变换平面与原来是平行的,可以得到:
不为0时,得到:
求解出的变换矩阵就可以将一个正方形变换到四边形。反之,四边形变换到正方形也是一样的。于是,我们通过两次变换:四边形变换到正方形+正方形变换到四边形就可以将任意一个四边形变换到另一个四边形。
?opencv代码:
[cpp]?viewplaincopy
#include??
#include??
??
#pragma?comment(lib,?"cv.lib")??
#pragma?comment(lib,?"cxcore.lib")??
#pragma?comment(lib,?"highgui.lib")??
??
int?main()??
{??
????CvPoint2D32f?srcTri[4],?dstTri[4];??
????CvMat???????warp_mat?=?cvCreateMat?(3,?3,?CV_32FC1);??
????IplImage????src?=?NULL;??
????IplImage????dst?=?NULL;??
??
????src?=?cvLoadImage?("test.png",?1);??
????dst?=?cvCloneImage?(src);??
????dst->origin?=?src->origin;??
????cvZero?(dst);??
??
????srcTri[0].x?=?0;??
????srcTri[0].y?=?0;??
????srcTri[1].x?=?src->width?-?1;??
????srcTri[1].y?=?0;??
????srcTri[2].x?=?0;??
????srcTri[2].y?=?src->height?-?1;??
????srcTri[3].x?=?src->width?-?1;??
????srcTri[3].y?=?src->height?-?1;??
??
????dstTri[0].x?=?src->width??0.05;??
????dstTri[0].y?=?src->height??0.33;??
????dstTri[1].x?=?src->width??0.9;??
????dstTri[1].y?=?src->height??0.25;??
????dstTri[2].x?=?src->width??0.2;??
????dstTri[2].y?=?src->height??0.7;??
????dstTri[3].x?=?src->width??0.8;??
????dstTri[3].y?=?src->height??0.9;??
??
????cvGetPerspectiveTransform?(srcTri,?dstTri,?warp_mat);??
????cvWarpPerspective?(src,?dst,?warp_mat);??
??
????cvNamedWindow("src",?1);??
????cvShowImage("src",?src);??
????cvNamedWindow?("Affine_Transform",?1);??
????cvShowImage?("Affine_Transform",?dst);??
??
????cvWaitKey?(0);??
??
????cvReleaseImage?(&src);??
????cvReleaseImage?(&dst);??
????cvReleaseMat?(&warp_mat);??
??
????return?0;??
}??
今天遇到一个问题是关于仿射变换的,但是由于没有将仿射变换的具体原理型明白,看别人的代码看的很费解,最后终于在师兄的帮助下将原理弄明白了,我觉得最重要的是理解仿射变换可以看成是几种简单变换的复合实现,
具体实现形式即将几种简单变换的变换矩阵M相乘,这样就很容易理解啦
?
定义:仿射变换的功能是从二维坐标到二维坐标之间的线性变换,且保持二维图形的“平直性”和“平行性”。仿射变换可以通过一系列的原子变换的复合来实现,包括平移,缩放,翻转,旋转和剪切。
这类变换可以用一个33的矩阵M来表示,其最后一行为(0,0,1)。该变换矩阵将原坐标为(x,y)变换为新坐标(x'',y''),
即
opencv中相应的函数是:
void?warpAffine(InputArray?src,OutputArray?dst,InputArray?M,Size?dsize,int?flags=INTER_LINEAR,int?borderMode=BORDER_CONSTANT,constScalar&?borderValue=Scalar())?
Parameters:
src?–inputimage.
dst?–outputimagethathasthesize?dsize?andthesametypeas?src?.
M?–??transformationmatrix,最重要的东东了,本文中着重讲M的构造
dsize?–sizeoftheoutputimage.
flags?–combinationofinterpolationmethods(see?resize()?)andtheoptionalflag?WARP_INVERSE_MAP?thatmeansthat?M?istheinversetransformation(??).
borderMode?–pixelextrapolationmethod(see?borderInterpolate());when?borderMode=BORDER_TRANSPARENT?,itmeansthatthepixelsinthedestinationimagecorrespondingtothe“outliers”inthesourceimagearenotmodifiedbythefunction.
borderValue?–valueusedincaseofaconstantborder;bydefault,itis0.
?下面介绍一些典型的仿射变换:
(1)平移,将每一点移到到(x+t,y+t),变换矩阵为
(2)缩放变换?将每一点的横坐标放大或缩小sx倍,纵坐标放大(缩小)到sy倍,变换矩阵为
(3)旋转变换原点:目标图形围绕原点顺时针旋转Θ弧度,变换矩阵为
(4)旋转变换?:目标图形以(x,y)为轴心顺时针旋转θ弧度,变换矩阵为
?
相当于两次平移与一次原点旋转变换的复合,即先将轴心(x,y)移到到原点,然后做旋转变换,最后将图片的左上角置为图片的原点,即
有的人可能会说为什么这么复杂呢,那是因为在opencv的图像处理中,所有对图像的处理都是从原点进行的,而图像的原点默认为图像的左上角,而我们对图像作旋转处理时一般以图像的中点为轴心,因此就需要做如下处理
?如果你觉得这样很麻烦,可以使用opencv中自带的Mat?getRotationMatrix2D(Point2fcenter,doubleangle,doublescale)函数获得变换矩阵M,
center:旋转中心
angle:旋转弧度,一定要将角度转换成弧度
scale:缩放尺度
它得到的矩阵是:
?
opencv中还有一个函数:Mat?getAffineTransform(InputArray?src,InputArray?dst)?
它通过三组点对就可以获得它们之间的仿射变换,如果我们在一组图像变换中知道变换后的三组点,那么我们就可以利用该函数求得变换矩阵,然后对整张图片进行仿射变换
还有一种与仿射变换经常混淆的变换为透视变换,透视变换需要四组点对才能确定变换矩阵,由于仿射变换保持“平直性”与“平行性”,因此只需要三组点对,而透视变换没有这种约束,故需要四组点对
?
本文将openCV中的RANSAC代码全部挑选出来,进行分析和讲解,以便大家更好的理解RANSAC算法。代码我都试过,可以直接运行。
在计算机视觉和图像处理等很多领域,都需要用到RANSAC算法。openCV中也有封装好的RANSAC算法,以便于人们使用。关于RANSAC算法的一些应用,可以看我的另一篇博客:
利用SIFT和RANSAC算法(openCV框架)实现物体的检测与定位,并求出变换矩阵(findFundamentalMat和findHomography的比较)
但是前几天师弟在使用openCV自带的RANSAC算法时,发现实验的运行时间并不会随着输入数据的增加而增加,感觉和理论上的不太相符。所以我就花了点时间,把openCV中关于RANSAC的源代码全部复制出来研究了一下。以便我们更加清晰的了解RANSAC算法的实际运行过程。
首先看两个类
?
//模型估计的基类,提供了估计矩阵的各种虚函数
//置信度设为0。99循环次数设置为了2000
classCvModelEstimator2
{
public:
????CvModelEstimator2(int_modelPoints,CvSize_modelSize,int_maxBasicSolutions);
????virtual~CvModelEstimator2();
?
????virtualintrunKernel(constCvMatm1,constCvMatm2,CvMatmodel)=0;
????//virtualboolrunLMeDS(constCvMatm1,constCvMatm2,CvMatmodel,
??????????????????????????//CvMatmask,doubleconfidence=0.99,intmaxIters=2000);
????virtualboolrunRANSAC(constCvMatm1,constCvMatm2,CvMatmodel,
????????????????????????????CvMatmask,doublethreshold,
????????????????????????????doubleconfidence=0.99,intmaxIters=2000);
????virtualboolrefine(constCvMat,constCvMat,CvMat,int){returntrue;}
????//virtualvoidsetSeed(int64seed);
?
protected:
????virtualvoidcomputeReprojError(constCvMatm1,constCvMatm2,
?????????????????????????????????????constCvMatmodel,CvMaterror)=0;
????virtualintfindInliers(constCvMatm1,constCvMatm2,
?????????????????????????????constCvMatmodel,CvMaterror,
?????????????????????????????CvMatmask,doublethreshold);
????virtualboolgetSubset(constCvMatm1,constCvMatm2,
????????????????????????????CvMatms1,CvMatms2,intmaxAttempts=1000);
????virtualboolcheckSubset(constCvMatms1,intcount);
?
????CvRNGrng;
????intmodelPoints;
????CvSizemodelSize;
????intmaxBasicSolutions;
????boolcheckPartialSubsets;
};
//单应矩阵估计的子类
classCvHomographyEstimator:publicCvModelEstimator2
{
public:
????CvHomographyEstimator(intmodelPoints);
?
????virtualintrunKernel(constCvMatm1,constCvMatm2,CvMatmodel);
????virtualboolrefine(constCvMatm1,constCvMatm2,
?????????????????????????CvMatmodel,intmaxIters);
?????
?
protected:
????virtualvoidcomputeReprojError(constCvMatm1,constCvMatm2,
?????????????????????????????????????constCvMatmodel,CvMaterror);
}; 上面的两个类中,CvModelEstimator2是一个基类,从名字就可以看出,这个类是用来估计模型的。可以看到里面提供了许多虚函数,这些函数有许多,比如runRANSAC是利用RANSAC方法计算单应矩阵,而runLMeDS是利用LMeDS方法计算单应矩阵,我们这里仅仅讲解RANSAC方法,所以其他不需要的内容我就直接注释掉了
CvHomographyEstimator继承自CvModelEstimator2,同样的,从名字也就可以看出,这个类使用来估计单应矩阵的。
接下来是两个类的构造函数和析构函数,这个没啥好说的了,基本都是默认的。
?
4
本范例的代码主要都是?学习OpenCV——通过KeyPoints进行目标定位这篇博客提供的,然后在它的基础上稍加修改,检测keypoints点的检测器是SURF,获取描述子也是用到SURF来描述,而用到的匹配器是FlannBased,匹配的方式是Knn方式,最后通过findHomography寻找单映射矩阵,perspectiveTransform获得最终的目标,在这个过程中还通过单映射矩阵来进一步去除伪匹配,这里只是贴出代码和代码解析,至于原理还没弄得特别明白,希望接下来可以继续学习,学懂了算法原理再来补充。
1、代码实现
[cpp]?viewplain?copy
?
?
#include?"stdafx.h"??
#include?"opencv2/opencv.hpp"??
#include?????
#include?????
??
using?namespace?cv;????
using?namespace?std;????
Mat?src,frameImg;????
int?width;????
int?height;????
vector?srcCorner(4);????
vector?dstCorner(4);????
??
static?bool?createDetectorDescriptorMatcher(?const?string&?detectorType,?const?string&?descriptorType,?const?string&?matcherType,????
????Ptr&?featureDetector,????
????Ptr&?descriptorExtractor,????
????Ptr&?descriptorMatcher?)????
{????
????cout?<" ????if?(detectorType=="SIFT"||detectorType=="SURF")????
????????initModule_nonfree(www.sm136.com);????
????featureDetector?=?FeatureDetector::create(?detectorType?);????
????descriptorExtractor?=?DescriptorExtractor::create(?descriptorType?);????
????descriptorMatcher?=?DescriptorMatcher::create(?matcherType?);????
????cout?<">"?< ????bool?isCreated?=?!(?featureDetector.empty()?||?descriptorExtractor.empty()?||?descriptorMatcher.empty()?);????
????if(?!isCreated?)????
????????cout?<"Can?not?create?feature?detector?or?descriptor?extractor?or?descriptor?matcher?of?given?types."?<"?< ????return?isCreated;????
}????
??
??
bool?refineMatchesWithHomography(const?std::vector&?queryKeypoints,??????
????const?std::vector&?trainKeypoints,???????
????float?reprojectionThreshold,??????
????std::vector&?matches,??????
????cv::Mat&?homography??)????
{????
????const?int?minNumberMatchesAllowed?=?4;??????
????if?(matches.size()? ????????return?false;??????
????//?Prepare?data?for?cv::findHomography??????
????std::vector?queryPoints(matches.size());??????
????std::vector?trainPoints(matches.size());??????
????for?(size_t?i?=?0;?i? ????{??????
????????queryPoints[i]?=?queryKeypoints[matches[i].queryIdx].pt;??????
????????trainPoints[i]?=?trainKeypoints[matches[i].trainIdx].pt;??????
????}??????
????//?Find?homography?matrix?and?get?inliers?mask??????
????std::vector?inliersMask(matches.size());??????
????homography?=?cv::findHomography(queryPoints,???????
????????trainPoints,???????
????????CV_FM_RANSAC,???????
????????reprojectionThreshold,???????
????????inliersMask);??????
????std::vector?inliers;??????
????for?(size_t?i=0;?i ????{??????
????????if?(inliersMask[i])??????
????????????inliers.push_back(matches[i]);??????
????}??????
????matches.swap(inliers);????
????Mat?homoShow;????
????drawMatches(src,queryKeypoints,frameImg,trainKeypoints,matches,homoShow,Scalar::all(-1),CV_RGB(255,255,255),Mat(),2);?????????
????imshow("homoShow",homoShow);?????
????return?matches.size()?>?minNumberMatchesAllowed;?????
??
}????
??
??
bool?matchingDescriptor(const?vector&?queryKeyPoints,const?vector&?trainKeyPoints,????
????const?Mat&?queryDescriptors,const?Mat&?trainDescriptors,?????
????Ptr&?descriptorMatcher,????
????bool?enableRatioTest?=?true)????
{????
????vector>?m_knnMatches;????
????vectorm_Matches;????
??
????if?(enableRatioTest)????
????{????
????????cout<<"KNN?Matching"< ????????const?float?minRatio?=?1.f?/?1.5f;????
????????descriptorMatcher->knnMatch(queryDescriptors,trainDescriptors,m_knnMatches,2);????
????????for?(size_t?i=0;?i ????????{????
????????????const?cv::DMatch&?bestMatch?=?m_knnMatches[i][0];????
????????????const?cv::DMatch&?betterMatch?=?m_knnMatches[i][1];????
????????????float?distanceRatio?=?bestMatch.distance?/?betterMatch.distance;????
????????????if?(distanceRatio? ????????????{????
????????????????m_Matches.push_back(bestMatch);????
????????????}????
????????}????
??
????}????
????else????
????{????
????????cout<<"Cross-Check"< ????????Ptr?BFMatcher(new?cv::BFMatcher(cv::NORM_HAMMING,?true));????
????????BFMatcher->match(queryDescriptors,trainDescriptors,?m_Matches?);????
????}????
????Mat?homo;????
????float?homographyReprojectionThreshold?=?1.0;????
????bool?homographyFound?=?refineMatchesWithHomography(????
????????queryKeyPoints,trainKeyPoints,homographyReprojectionThreshold,m_Matches,homo);????
??
????if?(!homographyFound)????
????????return?false;????
????else????
????{????
????????if?(m_Matches.size()>10)??
????????{??
????????????std::vector?obj_corners(4);??
????????????obj_corners[0]?=?cvPoint(0,0);?obj_corners[1]?=?cvPoint(?src.cols,?0?);??
????????????obj_corners[2]?=?cvPoint(?src.cols,?src.rows?);?obj_corners[3]?=?cvPoint(?0,?src.rows?);??
????????????std::vector?scene_cornwww.shanxiwang.neters(4);??
????????????perspectiveTransform(?obj_corners,?scene_corners,?homo);??
????????????line(frameImg,scene_corners[0],scene_corners[1],CV_RGB(255,0,0),2);????
????????????line(frameImg,scene_corners[1],scene_corners[2],CV_RGB(255,0,0),2);????
????????????line(frameImg,scene_corners[2],scene_corners[3],CV_RGB(255,0,0),2);????
????????????line(frameImg,scene_corners[3],scene_corners[0],CV_RGB(255,0,0),2);????
????????????return?true;????
????????}??
????????return?true;??
????}????
??
??
}????
int?main()????
{????
????string?filename?=?"box.png";????
????src?=?imread(filename,0);????
????width?=?src.cols;????
????height?=?src.rows;????
????string?detectorType?=?"SIFT";????
????string?descriptorType?=?"SIFT";????
????string?matcherType?=?"FlannBased";????
??
????Ptr?featureDetector;????
????Ptr?descriptorExtractor;????
????Ptr?descriptorMatcher;????
????if(?!createDetectorDescriptorMatcher(?detectorType,?descriptorType,?matcherType,?featureDetector,?descriptorExtractor,?descriptorMatcher?)?)????
????{????
????????cout<<"Creat?Detector?Descriptor?Matcher?False!"< ????????return?-1;????
????}????
????//Intial:?read?the?pattern?img?keyPoint????
????vector?queryKeypoints;????
????Mat?queryDescriptor;????
????featureDetector->detect(src,queryKeypoints);????
????descriptorExtractor->compute(src,queryKeypoints,queryDescriptor);????
??
????VideoCapture?cap(0);?//?open?the?default?camera????
????cap.set(?CV_CAP_PROP_FRAME_WIDTH,320);??
????cap.set(?CV_CAP_PROP_FRAME_HEIGHT,240?);??
????if(!cap.isOpened())??//?check?if?we?succeeded????
????{????
????????cout<<"Can''t?Open?Camera!"< ????????return?-1;????
????}????
????srcCorner[0]?=?Point(0,0);????
????srcCorner[1]?=?Point(width,0);????
????srcCorner[2]?=?Point(width,height);????
????srcCorner[3]?=?Point(0,height);????
??
????vector?trainKeypoints;????
????Mat?trainDescriptor;????
??
????Mat?frame,grayFrame;??
????char?key=0;????
??
????//??frame?=?imread("box_in_scene.png");????
????while?(key!=27)????
????{????
????????cap>>frame;????
????????if?(!frame.empty())??
????????{??
????????????frame.copyTo(frameImg);??
????????????printf("%d,%d\n",frame.depth(),frame.channels());??
????????????grayFrame.zeros(frame.rows,frame.cols,CV_8UC1);??
????????????cvtColor(frame,grayFrame,CV_BGR2GRAY);????
????????????trainKeypoints.clear();????
????????????trainDescriptor.setTo(0);????
????????????featureDetector->detect(grayFrame,trainKeypoints);????
??
????????????if(trainKeypoints.size()!=0)????
????????????{????
????????????????descriptorExtractor->compute(grayFrame,trainKeypoints,trainDescriptor);????
??
????????????????bool?isFound?=?matchingDescriptor(queryKeypoints,trainKeypoints,queryDescriptor,trainDescriptor,descriptorMatcher);????
????????????????imshow("foundImg",frameImg);????
??
????????????}????
????????}??
????????key?=?waitKey(1);?????
????}????
????cap.release();??
????return?0;??
}????
|
|