首先建议阅读图像旋转算法原理-旋转矩阵,这篇博客可以让你很好地理解图像中的每一个点是如何进行旋转操作的。其中涉及到了图像原点与笛卡尔坐标原点之间的相互转换以及点旋转的一些公式推导。 这里以图像围绕任意点(center_x, center_y)旋转为例,但是图像的原点在左上角,在计算的时候首先需要将左上角的原点移到图像中心,并且Y轴需要翻转。
而在旋转的过程一般使用旋转中心为坐标原点的笛卡尔坐标系,所以图像旋转的第一步就是坐标系的变换。(x’,y’)是笛卡尔坐标系的坐标,(x,y)是图像坐标系的坐标,经过坐标系变换后
坐标系变换到以旋转中心为原点后,接下来就要对图像的坐标进行变换。
逆变换是
由于在旋转的时候是以旋转中心为坐标原点的,旋转结束后还需要将坐标原点移到图像左上角,也就是还要进行一次变换。
上边两图,可以清晰的看到,旋转前后图像的左上角,也就是坐标原点发生了变换。
在求图像旋转后左上角的坐标前,先来看看旋转后图像的宽和高。从上图可以看出,旋转后图像的宽和高与原图像的四个角旋转后的位置有关。
我们将这个四个角点记为 transLeftTop, transRightTop, transLeftBottom, transRightBottom
设top为旋转后最高点的纵坐标 top = min({ transLeftTop.y, transRightTop.y, transLeftBottom.y, transRightBottom.y });
down为旋转后最低点的纵坐标 down = max({ transLeftTop.y, transRightTop.y, transLeftBottom.y, transRightBottom.y });
left为旋转后最左边点的横坐标 left = min({ transLeftTop.x, transRightTop.x, transLeftBottom.x, transRightBottom.x });
right为旋转后最右边点的横坐标 right = max({ transLeftTop.x, transRightTop.x, transLeftBottom.x, transRightBottom.x });
旋转后的宽和高为newWidth,newHeight,则可得到下面的关系:
旋转完成后要将坐标系转换为以图像的左上角为坐标原点,可由下面变换关系得到:
逆变换
综合以上,也就是说原图像的像素坐标要经过三次的坐标变换:
可以得到下面的旋转公式:(x’,y’)旋转后的坐标,(x,y)原坐标,(x0,y0)旋转中心,a旋转的角度(顺时针)
这种由输入图像通过映射得到输出图像的坐标,是向前映射。常用的向后映射是其逆运算
最后附上代码
void imageRotation(Mat& srcImage, Mat& dstImage, Mat_<double>& shape, float& angle)
{
const double cosAngle = cos(angle);
const double sinAngle = sin(angle);
// 计算标注中心
double center_x = 0;
double center_y = 0;
for (int i = 0; i < shape.rows; i++){
center_x += shape(i, 0);
center_y += shape(i, 1);
}
center_x /= shape.rows;
center_y /= shape.rows;
//原图像四个角的坐标变为以旋转中心的坐标系
Point2d leftTop(-center_x, center_y); //(0,0)
Point2d rightTop(srcImage.cols - center_x, center_y); // (width,0)
Point2d leftBottom(-center_x, -srcImage.rows + center_y); //(0,height)
Point2d rightBottom(srcImage.cols - center_x, -srcImage.rows + center_y); //(width,height)
//以center为中心旋转后四个角的坐标
Point2d transLeftTop, transRightTop, transLeftBottom, transRightBottom;
transLeftTop = rotationPoint(leftTop, cosAngle, sinAngle);
transRightTop = rotationPoint(rightTop, cosAngle, sinAngle);
transLeftBottom = rotationPoint(leftBottom, cosAngle, sinAngle);
transRightBottom = rotationPoint(rightBottom, cosAngle, sinAngle);
//计算旋转后图像的width,height
double left = min({ transLeftTop.x, transRightTop.x, transLeftBottom.x, transRightBottom.x });
double right = max({ transLeftTop.x, transRightTop.x, transLeftBottom.x, transRightBottom.x });
double top = min({ transLeftTop.y, transRightTop.y, transLeftBottom.y, transRightBottom.y });
double down = max({ transLeftTop.y, transRightTop.y, transLeftBottom.y, transRightBottom.y });
int width = static_cast<int>(abs(left - right) + 0.5);
int height = static_cast<int>(abs(top - down) + 0.5);
// 分配内存空间
dstImage.create(height, width, srcImage.type());
const double dx = -abs(left) * cosAngle - abs(down) * sinAngle + center_x;
const double dy = abs(left) * sinAngle - abs(down) * cosAngle + center_y;
int x, y;
for (int i = 0; i < height; i++) // y
{
for (int j = 0; j < width; j++) // x
{
//坐标变换
x = float(j)*cosAngle + float(i)*sinAngle + dx;
y = float(-j)*sinAngle + float(i)*cosAngle + dy;
if ((x<0) || (x >= srcImage.cols) || (y<0) || (y >= srcImage.rows))
{
if (srcImage.channels() == 3)
{
dstImage.at<cv::Vec3b>(i, j) = cv::Vec3b(0, 0, 0);
}
else if (srcImage.channels() == 1)
{
dstImage.at<uchar>(i, j) = 0;
}
}
else
{
if (srcImage.channels() == 3)
{
dstImage.at<cv::Vec3b>(i, j) = srcImage.at<cv::Vec3b>(y, x);
}
else if (srcImage.channels() == 1)
{
dstImage.at<uchar>(i, j) = srcImage.at<uchar>(y, x);
}
}
}
}
}
Point2d rotationPoint(Point2d srcPoint, const double cosAngle, const double sinAngle)
{
Point2d dstPoint;
dstPoint.x = srcPoint.x * cosAngle + srcPoint.y * sinAngle;
dstPoint.y = -srcPoint.x * sinAngle + srcPoint.y * cosAngle;
return dstPoint;
}