
1 旋轉矩形
首先建議閱讀圖像旋轉算法原理-旋轉矩陣,這篇博客可以讓你很好地理解圖像中的每一個點是如何進行旋轉操作的。其中涉及到了圖像原點與笛卡爾坐標原點之間的相互轉換以及點旋轉的一些公式推導。 這里以圖像圍繞任意點(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,則可得到下面的關系:

旋轉完成后要將坐標系轉換為以圖像的左上角為坐標原點,可由下面變換關系得到:

逆變換

綜合以上,也就是說原圖像的像素坐標要經過三次的坐標變換:
- 將坐標原點由圖像的左上角變換到旋轉中心
- 以旋轉中心為原點,圖像旋轉角度a
- 旋轉結束后,將坐標原點變換到旋轉后圖像的左上角
可以得到下面的旋轉公式:(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; }


