将图像原地址旋转 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 度为例):

  1. 起初 $J[0][0] \leftrightarrow I[1][0]$ 进行交换,即内存空间中元素 1 和 4 交换位置 $[{\color{red}1},2,3,4,5,6]$
  2. 紧接着 $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;
}