将图像原地址旋转 90 度,不而外开辟新空间。此题乍一看似乎平易,然后仔细研究发现着实并不容易。难点一在于通常图像的大小为 $M \times N$ 而非方阵 $N \times N$ ;难点二在于不允许而外开辟空间的情况就只能做值交换,然而旋转交换过程中由存在先后顺序,原先所在位置空间的值可能在先前已经被交换到别的位置,因此使得问题变得复杂化。
简单示例
由简入难,首先假设旋转一个 $2 \times 3$ 的矩阵,其原始矩阵 $I = \begin{bmatrix}1 & 2 & 3 \newline 4 & 5 & 6\end{bmatrix}$ ,顺时针旋转 90 度后的目标矩阵为 $J = \begin{bmatrix} 4 & 1 \newline 5 & 2 \newline 6 & 3 \end{bmatrix}$ (亦或是逆时针旋转 90 度后 $J = \begin{bmatrix} 3 & 6 \newline 2 & 5 \newline 1 & 4 \end{bmatrix}$ ),假设 $I, J$ 指向同一块内存空间,该内存大小为 “6” 的空间内。由此,我们需要推算 $I \to J$ 的变换关系。易得:
- 顺时针旋转 90 度:$J[row][col] = I[M - 1 - col][row]$ 。
- 逆时针旋转 90 度:$J[row][col] = I[col][N - 1 - row]$ 。
好了,如果 $J$ 是一块新开辟的空间,那么变量 $J$ 的空间按照上面公式取 $I$ 对应元素赋值即可。但由于 $I, J$ 公用一块内存空间,仅仅使用这个简单的策略会出现如下问题(以顺时针旋转 90 度为例):
- 起初 $J[0][0] \leftrightarrow I[1][0]$ 进行交换,即内存空间中元素 1 和 4 交换位置 $[{\color{red}1},2,3,4,5,6]$
- 紧接着 $J[0][1] \leftrightarrow I[0][0]$ 进行交换,即内存空间中元素 2 应该和原先元素 1 交换,但是该位置由于前一步现在已经不是 1 而是 4 了 $[{\color{green}4}, {\color{red}2},3,{\color{green}1},5,6]$
- 注意: $I, J$ 指向同一块内存大小为 “6” 的空间,假设首地址为 $P$ ,进行一维化有:$J[j_{row}][j_{col}] = P[j_{row} \times M + j_{col}]$ ,$I[i_{row}][i_{col}] = P[i_{row} \times N +i_{col}]$ 。
算法思路
根据上文中的示例,我们发现了问题所在。因此,我们需要有一种新的策略,如果发现待交换位置的元素已经被更换了,需要继续查找到该元素被交换到哪里去了。于是有了新的算法思路,伪代码如下(以顺时针旋转 90 度为例):
for j_row = 0 : N-1
for j_col = 0 : M-1
%% J[j_row][j_col] 前的元素均已正确
i_row = M-1-j_col;
i_col = j_row;
if j_row*M+j_col <= i_row*N+i_col %% 待替换元素在 J[j_row][j_col] 位置后面
swap(J[j_row][j_col], I[i_row][i_col]);
else %% 待替换元素在 J[j_row][j_col] 位置前面,已经被替换过了,需要迭代查找该元素被替换到哪里了,肯定在 J[j_row][j_col] 位置后面
while j_row*M+j_col > i_row*N+i_col
pos = i_row*N+i_col;
tmp_row = pos/M;
tmp_col = pos%M;
i_row = M-1-tmp_col;
i_col = tmp_row;
end
swap(J[j_row][j_col], I[i_row][i_col]);
end
end
end
由此,对于上述示例我们给出一个 python 代码如下(以顺时针旋转 90 度为例):
import numpy as np
# 申请一块存放 [1 2 3 4 5 6] 的内存空间
P = np.linspace(1, 6, 6)
# I, J 公用内存空间
I = P.reshape(2, 3) # I 为 2x3 的矩阵
J = P.reshape(3, 2) # J 为 3x2 的矩阵
print(I)
M = I.shape[0]
N = I.shape[1]
for j_row in range(N):
for j_col in range(M):
i_row = M-1-j_col
i_col = j_row
while j_row*M+j_col > i_row*N+i_col:
pos = i_row*N+i_col
tmp_row = int(pos/M)
tmp_col = int(pos%M)
i_row = M-1-tmp_col
i_col = tmp_row
J[j_row][j_col],I[i_row][i_col] = I[i_row][i_col],J[j_row][j_col]
print(J)
代码实现
有了先前的基础,接下来实现居于 OpenCV 的 C++ 代码。(以逆时针旋转 90 度为例)
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui.hpp>
int main(int argc, char** argv) {
cv::Mat I = cv::imread("../test.png", cv::IMREAD_UNCHANGED);
const int M = I.rows, N = I.cols;
const int channels = I.channels();
cv::imshow("I", I);
cv::Mat J = I; // I, J 公用内存空间
J = J.reshape(channels, N);
for (int j_row = 0; j_row < J.rows; ++j_row) {
for (int j_col = 0; j_col < J.cols; ++j_col) {
int i_row = j_col;
int i_col = N-1-j_row;
while (j_row*M+j_col > i_row*N+i_col) {
int pos = i_row*N+i_col;
int tmp_row = pos/M;
int tmp_col = pos%M;
i_row = tmp_col;
i_col = N-1-tmp_row;
}
switch (channels) {
case 1:
cv::swap<uchar>(J.at<uchar>(j_row, j_col), I.at<uchar>(i_row, i_col));
break;
case 3:
cv::swap<cv::Vec3b>(J.at<cv::Vec3b>(j_row, j_col), I.at<cv::Vec3b>(i_row, i_col));
break;
case 4:
cv::swap<cv::Vec4b>(J.at<cv::Vec4b>(j_row, j_col), I.at<cv::Vec4b>(i_row, i_col));
default:
break;
}
}
}
cv::imshow("J", J);
cv::waitKey();
cv::destroyAllWindows();
return 0;
}