做图像配准的时候,发现图像进行旋转的情况下的配准有一些特殊。于是想到可以用极坐标进行配准。查了一下资料,发现大家用的更多的是对数极坐标Log Polar。
笛卡尔坐标系和极坐标系
先来说一下我们常用的笛卡尔坐标。X轴水平向右是正方向,y轴垂直于x轴,竖直向上是正方向。但是在计算机中图像的原点在左上方,所以如果是在笛卡尔坐标中进行旋转,需要三个矩阵相乘,分别实现从计算机坐标到笛卡尔坐标,旋转角度theta,笛卡尔坐标转换为计算机坐标系。
如果将笛卡尔坐标系换为极坐标系,两个坐标轴分别是角度theta和长轴,点的位置的表示方式用与原点水平方向的夹角和到原点的距离表示。而对数极坐标,是在极坐标的基础上,对长轴的长度取对数,这样把到原点距离的线性变换变成非线性变换,当距离成倍增长时对数极坐标下的距离小幅变化,起到了数据压缩的作用,减少了计算量。就像是通信脉冲采样中的谬率和a率,还有信道中的信噪比。参考资料中提到的极坐标和对数坐标都是对人眼的仿生模拟,因为人眼的中心凸起有聚焦的作用,极坐标下的像素都环绕在原点周围,但考虑到半径越来越大,越远离中心点的地方空间分辨率越低。所以可以将这么一次变换看作是图像通过了如下的滤波器:
极坐标在OpenCV中的函数
OpenCV中集成了对数极坐标的实现。参数也都很简单:
cvLogPolar(
const CvArr* src, CvArr* dst,
CvPoint2D32f center, //极坐标变换的原点
double M,//缩放系数
int flagsCV_DEFAULT(CV_INTER_LINEAR+CV_WARP_FILL_OUTLIERS)//插值方式
);
在计算机图像处理中,看似目标图像是原图像经过某种变换直接得到的,其实我们不能一次性得到目标图像,而是在已知目标图像大小和位深的基础上,根据变换关系将目标图像的像素一个个映射回原图像,由原图像的已知像素求出目标图像的未知像素。而具体求解的算法就是插值法。这里cvLogPolar中默认的插值算法是双线性插值,策略是当目标图像的像素映射到原图边界之外时置0.具体关于函数的细节可以参考opencv手册https://docs.opencv.org/2.4/index.html
除了cvLogPolar函数,opencv还有一个函数cvLinerPolar,这个函数没有对距离取对数,所以叫线性极坐标。有的博客说是对半径做了log变换模拟人眼看到中间分辨率高,边缘分辨率低的效果,在机器学习中线性极坐标变换更加常用。下面是使用线性极坐标变换的例子,可以看到与对数极坐标相比,差距不大,都达到了人眼的效果,只是取了对数之后效果更加突出。
对极坐标变换结果的解释
因为在对数极坐标下两个坐标轴分别是角度和到中心点的距离的对数,所以原图中以中心点为圆心的圆在极坐标下映射为一条直线,通过中心点的直线被映射为平行于极轴坐标的直线(角度不变),原图边界处的点到中心点的距离在一个闭区间内有规律地波动,角度分量则在0~360度均匀分布。
相关代码
#include
#include
using namespace std;
IplImage* rotateImage1(IplImage* img, int degree);
int main()
{
IplImage *src = cvLoadImage("beaver.png");// , -1);cvLogPolar要求必须是三通道的
IplImage *dst = cvCreateImage(cvGetSize(src), 8, 3);
IplImage *dstLiner = cvCreateImage(cvGetSize(src), 8, 3);
IplImage* src2 = cvCreateImage(cvGetSize(src), 8, 3);
IplImage* src2Liner = cvCreateImage(cvGetSize(src), 8, 3);
CvPoint2D32f center = cvPoint2D32f(src->width/2, src->height/2);
//旋转中心为图像中心
//CvPoint2D32f center;
//center.x = float(img->width / 2.0 + 0.5);
//center.y = float(img->height / 2.0 + 0.5);这里为什么要加0.5?
double m = 50.0;
cvLogPolar(src, dst, center, m, CV_INTER_LINEAR + CV_WARP_FILL_OUTLIERS);
cvLogPolar(dst, src2, center, m, CV_INTER_LINEAR + CV_WARP_INVERSE_MAP);
//CV_WARP_INVERSE_MAP - 表示矩阵由输出图像到输入图像的逆变换,并且因此可以直接用于像素插值。否则,函数从map_matrix中寻找逆变换。
cvLogPolar(src, dstLiner, center, m, CV_INTER_LINEAR + CV_WARP_FILL_OUTLIERS);
cvLogPolar(dstLiner, src2Liner, center, m, CV_INTER_LINEAR + CV_WARP_INVERSE_MAP);
//对原图旋转
IplImage *RotateImage = rotateImage1(src, 20);
cvNamedWindow("旋转图", CV_WINDOW_AUTOSIZE);
cvShowImage("旋转图", RotateImage);
//对旋转图极坐标变换
IplImage *dstR = cvCreateImage(cvGetSize(src), 8, 3);
cvLogPolar(RotateImage, dstR, center, m, CV_INTER_LINEAR + CV_WARP_FILL_OUTLIERS);
cvNamedWindow("旋转图极坐标", CV_WINDOW_AUTOSIZE);
cvShowImage("旋转图极坐标", dstR);
cvNamedWindow("原图", CV_WINDOW_AUTOSIZE);
cvShowImage("原图", src);
cvNamedWindow("变换后的图", CV_WINDOW_AUTOSIZE);
cvShowImage("变换后的图", dst);
cvNamedWindow("线性极坐标变换后的图", CV_WINDOW_AUTOSIZE);
cvShowImage("线性极坐标变换后的图", dstLiner);
cvNamedWindow("反变换后的图", CV_WINDOW_AUTOSIZE);
cvShowImage("反变换后的图", src2);
cvNamedWindow("线性反变换后的图", CV_WINDOW_AUTOSIZE);
cvShowImage("线性反变换后的图", src2Liner);
cvWaitKey();
cvReleaseImage(&src);
cvReleaseImage(&dst);
cvReleaseImage(&dstLiner);
cvReleaseImage(&src2Liner);
cvReleaseImage(&RotateImage);
cvReleaseImage(&dstR);
cvDestroyWindow("原图");
cvDestroyWindow("变换后的图");
cvDestroyWindow("线性极坐标变换后的图");
cvDestroyWindow("线性反变换后的图");
cvDestroyWindow("旋转图");
cvDestroyWindow("旋转图极坐标");
return 0;
}
//旋转图像内容不变,尺寸相应变大 https://blog.csdn.net/qingzai_/article/details/51095297
IplImage* rotateImage1(IplImage* img, int degree){
double angle = degree * CV_PI / 180.; // 弧度
double a = sin(angle), b = cos(angle);
int width = img->width;
int height = img->height;
int width_rotate = int(height * fabs(a) + width * fabs(b));
int height_rotate = int(width * fabs(a) + height * fabs(b));
//旋转数组map
// [ m0 m1 m2 ] ===> [ A11 A12 b1 ]
// [ m3 m4 m5 ] ===> [ A21 A22 b2 ]
float map[6];
CvMat map_matrix = cvMat(2, 3, CV_32F, map);
// 旋转中心
CvPoint2D32f center = cvPoint2D32f(width / 2, height / 2);
cv2DRotationMatrix(center, degree, 1.0, &map_matrix);
map[2] += (width_rotate - width) / 2;
map[5] += (height_rotate - height) / 2;
IplImage* img_rotate = cvCreateImage(cvSize(width_rotate, height_rotate), 8, 3);
//对图像做仿射变换
//CV_WARP_FILL_OUTLIERS - 填充所有输出图像的象素。
//如果部分象素落在输入图像的边界外,那么它们的值设定为 fillval.
//CV_WARP_INVERSE_MAP - 指定 map_matrix 是输出图像到输入图像的反变换,
cvWarpAffine(img, img_rotate, &map_matrix, CV_INTER_LINEAR | CV_WARP_FILL_OUTLIERS, cvScalarAll(0));
return img_rotate;
}
需要注意的几个问题
1.Cvloadimage问题
在参考的代码中,因为要返回指向CvArr的指针,所以使用cvLoadImage读入图像,但是使用的是CV_LOAD_IMAGE_UNCHANGED参数,会中断。一个解释是:如果输入有冲突的标志,将采用较小的数字值。CV_LOAD_IMAGE_ANYCOLOR有着可以和CV_LOAD_IMAGE_ANYDEPTH同时使用的优点,CV_LOAD_IMAGE_ANYDEPTH | CV_LOAD_IMAGE_ANYCOLOR可以载入最真实的图像,所以CV_LOAD_IMAGE_UNCHANGED不再使用了。但是将cvLoadImage的参数换成CV_LOAD_IMAGE_ANYDEPTH| CV_LOAD_IMAGE_ANYCOLOR也依然中断。后来经过调试发现,中断发生在cvLogPolar阶段,源图像只要是单通道即灰度图就会中断。而我选择的原图就是单通道的,所以按照原图载入就会发生错误。三通道的图可以被读取成单通道的,而之前我不知道的是单通道的图也可以读取成三通道的。。
cvLoadImage和imread都调用的是imread_函数,只不过参数一个是LOAD_IMAGE,一个是LOAD_MAT。
enum
{
/* 8bit, color or not */
CV_LOAD_IMAGE_UNCHANGED =-1,
/* 8bit, gray */
CV_LOAD_IMAGE_GRAYSCALE =0,
/* ?, color 缺省值,三通道*/
CV_LOAD_IMAGE_COLOR =1,
/* any depth, 任意深度,保持不变? */
CV_LOAD_IMAGE_ANYDEPTH =2,
/* ?, any color 任意颜色,保持不变?*/
CV_LOAD_IMAGE_ANYCOLOR =4
};
2.反变换问题
cvLogPolar的CV_WARP_INVERSE_MAP参数可以实现反变换,但是链接5的代码中把反变换的输入图像写成笛卡尔坐标系下的原图了。
Reference:
1. https://blog.csdn.net/liyuan02/article/details/6750828
2. https://blog.csdn.net/xieyan0811/article/details/71106496
3. https://blog.csdn.net/jjrfjyfjyfjdfjrujdjd/article/details/40268573
4. Opencv代码cvloadimage:https://blog.csdn.net/w12345_ww/article/details/45362843
5. https://blog.csdn.net/hitwengqi/article/details/6895215
6. Unchanged:https://blog.csdn.net/smf0504/article/details/51384023