就是做图像像素点的位置变换,旋转后的位置的像素点赋值为原先像素点的值

旋转的原图像以图像左上角的顶点为原点,左上角的两条边分别为x轴和y轴建立图像坐标系。当然就算图像坐标系不是这样建立的,可以将原问题转换为这样建立图像坐标系的此问题再完成图像的图像坐标系到原问题的图像坐标系的变换

普通数学的运算

任意一点与旋转中心的连线,长度和与x轴的角度,伴随着旋转发生变化,进而计算出旋转后的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import cv2
import numpy as np
import math
import timeit

start=timeit.default_timer()

da=math.pi/4 # 旋转角度
cx,cy=(360,640) # 旋转中心点
img_name='test.jpg' # 原图像名
rotated_img_name='test1.jpg' # 旋转后的图像名

img=cv2.imread(img_name) # 读取图像
h,w=img.shape[:2] # 获取宽高

ends=np.array([[0,h-1,0,h-1],[0,0,w-1,w-1]],dtype=np.int16) # 图像四点的像素坐标,方便之后获取旋转后的像素坐标

A=np.array([[i-cx for _ in range(w)] for i in range(h)],dtype=np.float32)
B=np.array([[i-cy for i in range(w)] for _ in range(h)],dtype=np.float32) # 到旋转中心的x轴和y轴上的距离
start1=timeit.default_timer()
Dis=np.sqrt(A*A+B*B) # 到旋转中心的距离
Tan=np.divide(B,A,out=np.full(A.shape,np.nan),where=A!=0)
Ang=np.arctan(Tan,out=np.ones_like(A)*math.pi/2,where=~np.isnan(Tan))
Ang=np.where(A>=0,Ang,Ang+math.pi) # 与旋转中心的连线和x轴的夹角,第二、三象限的角需要加math.pi/2,因为arctan算出的角在第一、四象限
NX=Dis*np.cos(Ang+da)
NY=Dis*np.sin(Ang+da) # 旋转后的像素点的位置
end1=timeit.default_timer()
print(f'piecewise op {end1-start1} s')
minX,maxX,minY,maxY=math.floor(np.min(NX[ends[0],ends[1]])),math.floor(np.max(NX[ends[0],ends[1]])),math.floor(np.min(NY[ends[0],ends[1]])),math.floor(np.max(NY[ends[0],ends[1]])) # 确定旋转后的图像的在x轴和y轴上的上下界
nh,nw=maxX-minX+1,maxY-minY+1 # 旋转后的图像的宽高
res=np.ones([nh,nw,3],dtype=np.uint8)*255 # 初始化旋转后的图像

res[np.floor(NX).astype(np.int16)-minX,np.floor(NY).astype(np.int16)-minY]=img[(A+cx).astype(np.int16),(B+cy).astype(np.int16)] # 旋转前的像素点贴到旋转后的位置

cv2.imwrite(rotated_img_name,res) # 保存
# 展示代码,不好用,如果图像太大,窗口无法展示整张旋转后的图像,如果加上缩放,图像又变形。

end=timeit.default_timer()
print('cost time',end-start,'s')

旋转矩阵

设坐标轴ZBZ的x轴绕原点旋转θ_x,y轴绕原点旋转θ_y后变为坐标轴ZBZ’,则在坐标轴ZBZ下的点A(x,y)在坐标轴ZBZ’下的坐标表示为(x’,y’),有关系

np.matmul(np.array([[cosθ_x,sinθ_x],[-sinθ_y,cosθ_y]]),np.array([[x],[y]]))=np.array([[x’],[y’]])

np.array([[cosθ_x,sinθ_x],[-sinθ_y,cosθ_y]])就是旋转矩阵,一般情况下,坐标轴旋转的时候θ_x=θ_y

旋转矩阵的推导过程可通过三角函数和画图得出,此处不再赘述

由旋转矩阵的产生方法,我们对旋转问题进行新的构造,即旋转后的图像的图像坐标轴旋转到我们主视角的坐标轴,在旋转后的图像的图像坐标系中各个像素点的位置是正常视角的,即图像左上角的点位置坐标为(0,0),现在求这些点在新的旋转的图像坐标系(尽管来说这个“新的旋转的”才是我们的主视角)上的位置坐标,直接用旋转矩阵乘上去即可,旋转变换。

注意代码里的旋转矩阵套的θ为-da,再经过奇变偶不变,正负看象限,变换得来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import numpy as np
import cv2
import math
import timeit

start=timeit.default_timer()

da=math.pi/4
cx,cy=360,640
img_name='test.jpg'
rotated_img_name='test1.jpg'

img=cv2.imread(img_name)
h,w=img.shape[:2]
ends=np.array([[0,0,h-1,h-1],[0,w-1,0,w-1]],dtype=np.int16)

A=np.stack((np.array([[i-cx for _ in range(w)] for i in range(h)],dtype=np.float32),np.array([[i-cy for i in range(w)] for _ in range(h)],dtype=np.float32)),axis=0)
start1=timeit.default_timer()
B=np.matmul(np.array([[math.cos(da),-math.sin(da)],[math.sin(da),math.cos(da)]]),A.reshape(2,h*w)).reshape(2,h,w)
end1=timeit.default_timer()
print(f'matmul op {end1-start1} s')
minX,maxX,minY,maxY=math.floor(np.min(B[0,ends[0],ends[1]])),math.floor(np.max(B[0,ends[0],ends[1]])),math.floor(np.min(B[1,ends[0],ends[1]])),math.floor(np.max(B[1,ends[0],ends[1]]))
nh,nw=maxX-minX+1,maxY-minY+1
res=np.full((nh,nw,3),255,dtype=np.uint8)
res[np.floor(B[0,:,:]).astype(np.int16)-minX,np.floor(B[1,:,:]).astype(np.int16)-minY]=img[(A[0,:,:]+cx).astype(np.int16),(A[1,:,:]+cy).astype(np.int16)]

cv2.imwrite(rotated_img_name,res)

end=timeit.default_timer()
print('cost time',end-start,'s')

两种方法的对比

旋转矩阵比元素级别的多次计算快0.2s,故整体也差不多快这么多,尤其旋转矩阵方法中A是普通数学运算方法的A和B的拼接,基本上速度差不多。

旋转矩阵方法我初始化A的时候首先采用的代码是A=np.array([[[i-cx,j-cy] for j in range(w)] for i in range(h)]),好像这样做消耗更多的性能,花费的时间至少说是最终的那行代码的三倍,体感特别差,还有A还需要转置和reshape,拖累了整体的性能,害我差点以为矩阵乘方法没有元素级别的数学运算的方法快,在众人间闹出笑话,之后测了一下,就发现问题的源头在这个初始化,因为矩阵乘比元素操作快,其他位置的运算差不多,(一开始以为把大小为2的维度从第3维挪到第1维会加快速度呢,结果没有,差不多的时间,说明索引方法差不多,没有索引维度方面的优化),那改了A的初始化,速度快了很多,比普通数学运算快了。

之前我跟着参考链接的博客学线性变换的时候,考虑过如何做图像旋转,想不明白,昨天又看它介绍旋转矩阵,不行,这次我好歹也得把这玩意搞出来,就做出来普通数学运算的方法,今天和室友的交流,以及又看了一遍博客,才想明白旋转矩阵的方法。

参考链接:旋转矩阵

创建于2023.4.6/18.35,修改于2023.4.6/18.35