0 引言
在数字图像处理技术中,图像旋转算法是最基本的操作之一。本文实现一种快速的图像旋转算法,并和原始方法以及opencv提供的旋转方案进行速度上的比较。
1 基本原理
图像旋转有两种计算坐标的思路,分别是前向映射和反向映射。其中后向映射在插值运算时较为方便,前向映射时计算插值则需要先知道全局的映射关系。如下图所示,反向映射是从旋转后图像出发,寻找其在原图中的位置,如果该位置不是整数坐标的话,则利用周围像素值进行插值运算。同理前向映射也是类似,但是差别是,前向映射四周的像素灰度值在当前并不是已知的,所以要算完整图的权重后再进行归一化。
正向和反向映射的公式见:http://blog.csdn.net/lkj345/article/details/50555870 在网上所有的公式中,这个是比较靠谱的,考虑了笛卡尔坐标系和图像坐标系的差别。
2 优化方法
由上面的原理可以得到朴素的求解方法,以反向映射为例,计算旋转后图像中每个整数坐标在原图中的精确坐标。从上面给出的公式可以知道每个坐标点的计算有若干次浮点乘法和加法。有一个想法是,既然知道旋转角度,知道每行第一个元素的坐标,即可用图形学中直线绘制的算法计算出这一行中剩下点的坐标。例如bresenham直线算法、DDA直线算法等。
这样可以避免多次乘法运算,只计算每行第一个像素的精确坐标,剩下的坐标使用浮点加法计算(DDA)即可。更进一步的,只要知道原图第一行的精确坐标,剩下每一行的第一个坐标也可用此思想计算。这样整个图只有第一个像素点需要使用浮点乘法计算精确坐标,剩下的像素坐标可以通过浮点加法计算出来。
该思想具体内容可参考文献[1]。但是个人觉得其中伪代码的描述不算是bresenham算法,bresenham算法中利用一个递推的表达式完全去掉了浮点运算(包括乘法和加法),倒更像是DDA算法的思想。
3 代码清单
这里给出cpp版本的图像旋转实现
代码1 最近邻插值和双线性插值的实现
/*
% INTERPOLATE 插值方法
% f 原图 sz图像大小
% m 原图y整数坐标
% n 原图x整数坐标
% ex x和亚像素坐标误差
% ey x和亚像素坐标误差
% way 1为最近邻插值,2为双线性插值方法
*/
inline BYTE Interpolate(BYTE f[], int sz[], int m, int n, float ex, float ey, char way){
BYTE gray=0;
float fr1, fr2, fr3;
//1. 误差统一到0到1之间
if (ex < 0){
ex = 1 + ex;
n--;
}
if (ey < 0){
ey = 1 + ey;
m--;
}
if (m <= 0 || n <= 0)
return gray;
//2. 最邻近差值
if (way == 1){
if (ex > 0.5)
n++;
if (ey > 0.5)
m++;
if (m > sz[0] || n > sz[1])
return gray;
gray = f[sz[1]*m+n];
return gray;
}
// 3.双线性插值
if (((m + 1) > sz[0]) || ((n + 1) > sz[1]))
return gray;
if (way == 2){
fr1 = (1 - ex)*float(f[sz[1] * m + n]) + ex*float(f[sz[1] * m + n + 1]);
fr2 = (1 - ex)*float(f[sz[1] * (m + 1) + n]) + ex*float(f[sz[1] * (m+1) + n + 1]);
fr3 = (1 - ey)*fr1 + ey*fr2;
gray = BYTE(fr3);
}
return gray;
}
代码2 原始的反向映射算法
BYTE* normalRoate(BYTE img[],int w,int h,double theta,int *neww,int *newh){
float fsin, fcos,c1,c2,fx,fy,ex,ey;
int w1, h1,xx,yy;
int sz[2] = {h,w};
//1. 计算基本参数
fsin = sin(theta);
fcos = cos(theta);
*newh=h1 = ceilf(abs(h*fcos) + abs(w*fsin));
*neww=w1 = ceilf(abs(w*fcos) + abs(h*fsin));
auto I1 = new(std::nothrow) BYTE[w1*h1];
if (!I1)
return NULL;
memset(I1, 0, w1*h1);
c1 = (w - w1*fcos - h1*fsin) / 2;
c2 = (h + w1*fsin - h1*fcos) / 2;
//2. 计算反向坐标并计算插值
for (int y = 0; y < h1; y++){
for (int x = 0; x < w1; x++){
//计算后向映射点的精确位置 每个点都使用原始公式计算
fx = x*fcos + y*fsin + c1; //四次浮点乘法和四次浮点加法
fy = y*fcos - x*fsin + c2;
xx = roundf(fx);
yy = roundF(fy);
ex = fx - float(xx);
ey = fy - float(yy);
I1[w1*y+x] = Interpolate(img,sz,yy, xx, ex, ey, 2);//双线性插值
}
}
return I1;
}
代码3 基于直线算法的图像旋转
BYTE* DDARoateFast(BYTE img[], int w, int h, double theta, int *neww, int *newh){
float fsin, fcos, c1, c2, fx, fy, ex, ey;
int w1, h1, xx, yy;
int sz[2] = { h, w };
//1. 计算旋转参数
fsin = sin(theta);
fcos = cos(theta);
*newh = h1 = ceilf(abs(h*fcos) + abs(w*fsin));
*neww = w1 = ceilf(abs(w*fcos) + abs(h*fsin));
auto I1 = new(std::nothrow) BYTE[w1*h1];
if (!I1)
return NULL;
memset(I1, 0, w1*h1);
c1 = (w - w1*fcos - h1*fsin) / 2; //见文献[1]的公式
c2 = (h + w1*fsin - h1*fcos) / 2;
fx = c1-fsin;
fy = c2-fcos;
//2. 计算反向坐标并计算插值
for (int y = 0; y < h1; y++){//整个二层循环中计算坐标点时都没有浮点乘法运算
//计算第一个后向映射点的精确位置
fx = fx + fsin;
fy = fy + fcos;
// 计算第一个点对应的栅格位置
xx = roundf(fx);
yy = roundF(fy);
ex = fx - float(xx);//误差
ey = fy - float(yy);
for (int x = 0; x < w1; x++){
I1[w1*y + x] = InterpolateFast(img, sz, yy, xx, ex, ey, 2);
ex = ex + fcos;
ey = ey - fsin;
if (ex>0.5){
xx++;
ex = ex - 1;
}
if (ey < -0.5){
yy--;
ey = ey + 1;
}
}
}
return I1;
}
代码4 前向映射的opencv实现
void FastRotateImage(Mat &srcImg, Mat &roateImg, float degree){
assert((srcImg.cols > 0) && (srcImg.rows > 0));
float fsin, fcos, c1, c2, fx, fy, xx, yy;
int w1, h1,w,h;
w = srcImg.cols;
h = srcImg.rows;
int sz[2] = { srcImg.rows, srcImg.cols };
Mat map1_x, map2_y,m1,m2,m3,sPoint,newMap; //m1 m2 m3 为前文推荐博客中的基本矩阵
//1. 计算旋转参数
fsin = sin(degree);
fcos = cos(degree);
h1 = ceilf(abs(h*fcos) + abs(w*fsin));
w1 = ceilf(abs(w*fcos) + abs(h*fsin));
roateImg.create(h1, w1, CV_8UC1); //srcImg.type
map1_x.create(srcImg.size(), CV_32FC1);
map2_y.create(srcImg.size(), CV_32FC1);
c1 = (w - w1*fcos - h1*fsin) / 2;
c2 = (h + w1*fsin - h1*fcos) / 2;
//2. 计算前向的第一个点坐标
m1=Mat::eye(3, 3, CV_32FC1);
m1.at<float>(2, 0) =-w / 2;
m1.at<float>(2, 1) =h / 2;
m1.at<float>(1, 1) = -1;
m2=Mat::eye(3, 3, CV_32FC1);
m2.at<float>(0, 0) = fcos;
m2.at<float>(0, 1) = -fsin;
m2.at<float>(1, 0) = fsin;
m2.at<float>(1, 1) = fcos;
m3=Mat::eye(3, 3, CV_32FC1);
m3.at<float>(2, 0) =w1 / 2;
m3.at<float>(2, 1) =h1 / 2;
m3.at<float>(1, 1) = -1;
sPoint=Mat::zeros(1,3,CV_32FC1);
sPoint.at<float>(0, 2) = 1;
Mat snPoint = sPoint*m3*m2*m1;
//cout << snPoint << endl;
fx =snPoint.at<float>(0,0) -fsin;
fy = snPoint.at<float>(0, 1) - fcos;
//3. 用直线画法计算剩余其他的映射点坐标
for (int y = 0; y < h; y++){
//计算第一个前向映射点的精确位置
fx = fx + fsin;
fy = fy + fcos;
xx = fx-fcos;
yy = fy+fsin;
float *ptrx = map1_x.ptr<float>(y);
float *ptry = map2_y.ptr<float>(y);
for (int x = 0; x < w; x++){
xx = xx + fcos;
yy = yy - fsin;
*(ptrx++) = xx;
*(ptry++) = yy;
}
}
//3.利用opencv的重映射函数完成前向映射的插值运算
remap(srcImg, roateImg, map1_x, map2_y, INTER_CUBIC, BORDER_CONSTANT, Scalar(0, 0, 0));
}
代码5 opencv的旋转实现
//逆时针旋转图像degree角度(原尺寸)
void rotateImage(Mat &img, Mat &img_rotate, int degree)
{
//旋转中心为图像中心
CvPoint2D32f center;
center.x = float(img.cols / 2.0 + 0.5);
center.y = float(img.rows / 2.0 + 0.5);
//计算二维旋转的仿射变换矩阵
Mat M(2, 3, CV_32FC1);
M = getRotationMatrix2D(center, degree, 1);
clock_t t1 = clock();
//变换图像,并用黑色填充其余值
warpAffine(img, img_rotate, M, img.size(), INTER_CUBIC);
clock_t t2 = clock();
cout << (t2 - t1)*1.0 / CLOCKS_PER_SEC/10;
}
4 效率对比
在debug模式下,512*512的灰度图效率如下
代码编号 | 平均时间 |
---|---|
5 | 16ms |
4 | 19ms |
3 | 19ms |
2 | 41ms |
在release模式下,代码3和代码4大概是代码2的四倍。同时代码5的速度要更优于代码3。注意到后向映射更便于实现,且对旋转图像放大的操作也更自然。代码5和4中旋转后的图像和原图尺寸一致,若要获取完整的图像需要在原图四周加补充像素。总结,还是opencv的自带的仿射变换做旋转速度快,但是你若受限于条件不能使用opencv库,可以考虑这种快速旋转方法,比朴素方法在各个图像尺度上(512512,10241024,2048*2048)都快四倍。
参考文献
[1]:石慎, 张艳宁, 郗润平,等. 基于Bresenham画线算法的图像快速-高精度旋转算法[J]. 计算机辅助设计与图形学学报, 2007, 19(11):1387-1392.