1. 旋转原理
1)坐标平移
图像旋转一般都是围绕图像的中心进行旋转,但是图像是一个矩阵,它的原点是在左上角,所以我们得先将坐标平移到中心。如图所示。
原中心点为O1,需要平移到O2,平移后的O1坐标相对于O2应为(x1- W/2, -y1+H/2)
坐标平移计算方式:
设原图中的像素点为[X0,Y0,1] ,图像的宽为W,高为H,则变换后的坐标为[X1,Y1,1],计算公式为:
2)旋转角度计算
上面的公式推导讲的还是挺清楚的,所以最终的旋转计算公式如下, 其中[X2,Y2]是旋转后的坐标
3)还原坐标
根据前两个公式计算后可以得到旋转后的坐标点,但是得到的是数学坐标系,所以我们还得还原为图像坐标系,即第一步的反运算过程。可以参考这篇博客https://blog.csdn.net/liyuan02/article/details/6750828
还原后的坐标[X3,Y3]计算公式为:
最终计算公式如下:
2. 前向映射
所谓前向映射就是将计算得到的坐标点传递给旋转后的图像。当然通过上述的计算公式得到的坐标点肯定会有小数,但是图像中的坐标是整数型,所以我们会将计算得到的浮点型数转换为小数,这样会丢失很多图像的信息,导致图像图像会有很多空洞。代码如下。
注:1.我是从图像的高开始遍历图像,所以计算公式与上面的有点不同
2.我希望旋转后的图像大小是保持不变的,超出边界的部分则会丢弃。如果你希望旋转后的图像大小保持不变,则要重新计算旋转后图像的大小。newH = Hcosθ + Wsinθ,newW = Hsinθ + Wcosθ。因此我的代码中并没有重新计算图像大小。
#前向映射
def forward_rotate(imgArray):
H, W, channel = imgArray.shape
pi = math.pi
theta = random.randint(0,360) #得到随机旋转的角度
angle = theta * pi / 180
matrix1 = np.array([[1, 0, 0],
[0, -1, 0],
[-0.5 * H, 0.5 * W, 1]]) #先高后宽
matrix2 = np.array([[math.cos(angle), -math.sin(angle), 0],
[math.sin(angle), math.cos(angle), 0],
[0, 0, 1]])
matrix3 = np.array([[1, 0, 0],
[0, -1, 0],
[0.5 * H, 0.5 * W, 1]])
new_data = np.zeros_like(imgArray, dtype=np.uint8)
for i in range(H):
for j in range(W):
dot1 = np.matmul(np.array([i, j, 1]), matrix1) #坐标平移
dot2 = np.matmul(dot1, matrix2) #坐标旋转
dot3 = np.matmul(dot2, matrix3) #坐标还原
new_coordinate = dot3
new_i = int(math.floor(new_coordinate[0])) #取整
new_j = int(math.floor(new_coordinate[1]))
#判断是否超出边界或者坐标是否小于1,则丢失
if new_j>=W or new_i >=H or new_i<1 or new_j<1:
continue
new_data[new_i, new_j, :] = imgArray[i, j, :]
return new_data
图像旋转效果如下:坐左边是原图,右边是旋转后的图,可以看出旋转后的图比较模糊,因为丢失了很多像素信息。
3. 反向映射
所谓反向映射就是指从旋转后的图像出发,找到原图中对应的像素点,然后将原图中的像素点值传递进来。这样旋转后的图像的每个像素肯定可以对应到原图像中的一个点,然后通过插值算法使得图像更清晰完整。下面是反向映射的过程
1)最邻近插值
通过以下代码你可以发现和上面的前向映射几乎一模一样,唯一不同的是在最后一行,这样就很容易理解什么是前向映射和反向映射了。
#最邻近旋转
def nearest_rotate(imgArray):
H, W, channel = imgArray.shape
pi = math.pi
theta = random.randint(0,360)
angle = theta * pi / 180
matrix1 = np.array([[1, 0, 0],
[0, -1, 0],
[-0.5 * H, 0.5 * W, 1]])
matrix2 = np.array([[math.cos(angle), -math.sin(angle), 0],
[math.sin(angle), math.cos(angle), 0],
[0, 0, 1]])
matrix3 = np.array([[1, 0, 0],
[0, -1, 0],
[0.5 * H, 0.5 * W, 1]])
new_data = np.zeros_like(imgArray, dtype=np.uint8)
for i in range(H):
for j in range(W):
dot1 = np.matmul(np.array([i, j, 1]), matrix1)
dot2 = np.matmul(dot1, matrix2)
dot3 = np.matmul(dot2, matrix3)
new_coordinate = dot3
new_i = int(math.floor(new_coordinate[0]))
new_j = int(math.floor(new_coordinate[1]))
if new_j>=W or new_i >=H or new_i<1 or new_j<1:
continue
new_data[i, j, :] = imgArray[new_i, new_j, :]
return new_data
2)双线性插值
使用最近邻插值图像任然会有点模糊,所以采用双线性插值效果会好很多。这些插值算法可以参考这篇博客https://blog.csdn.net/weixin_42795611/article/details/111406043
#双线性旋转
def bilinear_rotate(imgArray):
H, W, channel = imgArray.shape
pi = math.pi
theta = random.randint(0,360)
angle = theta * pi / 180
matrix1 = np.array([[1, 0, 0],
[0, -1, 0],
[-0.5 * H, 0.5 * W, 1]])
matrix2 = np.array([[math.cos(angle), -math.sin(angle), 0],
[math.sin(angle), math.cos(angle), 0],
[0, 0, 1]])
matrix3 = np.array([[1, 0, 0],
[0, -1, 0],
[0.5 * H, 0.5 * W, 1]])
new_data = np.zeros_like(imgArray,dtype=np.uint8)
for i in range(H):
for j in range(W):
dot1 = np.matmul(np.array([i, j, 1]), matrix1)
dot2 = np.matmul(dot1, matrix2)
dot3 = np.matmul(dot2, matrix3)
new_coordinate = dot3
new_i = int(math.floor(new_coordinate[0]))
new_j = int(math.floor(new_coordinate[1]))
u = new_coordinate[0] - new_i
v = new_coordinate[1] - new_j
if new_j>=W or new_i >=H or new_i<1 or new_j<1 or (i+1)>=H or (j+1)>=W:
continue
if (new_i + 1)>=H or (new_j+1)>=W:
new_data[i, j, :] = imgArray[new_i,new_j, :]
else:
new_data[i, j, :] = (1-u)*(1-v)*imgArray[new_i,new_j, :] + \
(1-u)*v*imgArray[new_i,new_j+1, :] + \
u*(1-v)*imgArray[new_i+1,new_j, :] +\
u*v*imgArray[new_i+1,new_j+1, :]
return new_data
图像旋转效果如下所示,左边为原图,中间为最邻近插值,右边为双线性插值。可以看出双线性插值要清晰很多。
完整代码可查看python实现2D图像旋转插值算法