yolov5的數據增強中,透視、仿射變換統一使用了random_perspective一個函數進行處理,包含了旋轉、縮放、平移、剪切變換(shear,實際是按坐標軸方向變換,具體可看下文)、透視。其中下面這段代碼的這個參數有點疑惑,因此尋找了不少透視變換的資料,這里記錄我自己的思考。
仿射變換
仿射變換主要包括旋轉、縮放、平移、shear等,仿射變換矩陣可以由旋轉矩陣、平移矩陣等組合得到,仿射變換矩陣可以用如下矩陣表示。參考來源

旋轉、平移等基礎變換矩陣如下圖所示,random_perspective函數內部也是根據相應旋轉角度等參數構建相應的矩陣並組合起來。

透視變換
回到開始說的,yolov5源碼說的透視參數對應矩陣M[2,0],M[2,1],random_perspective函數也只是建議參數范圍0~0.001,前面的旋轉、平移、縮放、shear參數都有具體含義並得到相應的矩陣,透視變換相關的參數卻只是給出了數值范圍,因此困惑於這個參數具體代表什么含義?

查找很多資料,基本都是opencv怎么使用透視變換、或者怎么實現求解透視變換矩陣的問題(可參考鏈接)。我想知道的是,透視變換矩陣是怎樣由旋轉、平移等基本操作矩陣組合而來的,即矩陣M[2,0],M[2,1]參數是怎樣的操作得到的。

於是想到透視變換是把圖像投影到新的視平面,如上圖所示,新平面如果與原圖像平面平行那就是簡單的仿射變換,不平行那就是繞x/y軸發生了旋轉,即空間點的旋轉變換。空間坐標系轉換。
因此perspective參數應該是繞x,y軸旋轉矩陣產生。
測試程序如下
def __mylearn():
colors = [(0, 0, 255), (255, 0, 0)]#紅色繪制原始框,藍色繪制變換后的框
lw = 1
#voc數據集的一張圖片數據
img = cv2.imread('../examples/2008_000109.jpg')
src = img.copy()
h, w, c = img.shape
cx, cy = w / 2, h / 2
bboxs = np.loadtxt('../examples/2008_000109.txt')
cw, ch = 0.5 * bboxs[:, 3], 0.5 * bboxs[:, 4]
bboxs[:, 3] = bboxs[:, 1] + cw
bboxs[:, 4] = bboxs[:, 2] + ch
bboxs[:, 1] -= cw
bboxs[:, 2] -= ch
bboxs[:, [1, 3]] *= w
bboxs[:, [2, 4]] *= h
srcboxs = bboxs.round().astype(np.int)
#原始圖像繪制bbox框
for box in srcboxs:
s = f'c{box[0]}'
cv2.rectangle(src, (box[1], box[2]), (box[3], box[4]), color=colors[0], thickness=lw)
cv2.putText(src, s, (box[1], box[2] - 2), cv2.FONT_HERSHEY_COMPLEX, 1.0, color=colors[0], thickness=lw)
rotate = 10
shear = 5
scale = 0.8
R, T1, T2, S, SH = np.eye(3), np.eye(3), np.eye(3), np.eye(3), np.eye(3)
cos = math.cos(-rotate / 180 * np.pi) # 圖片坐標原點在左上角,該坐標系的逆時針與肉眼看照片方向相反
sin = math.sin(-rotate / 180 * np.pi)
R[0, 0] = R[1, 1] = cos # 旋轉矩陣
R[0, 1] = -sin
R[1, 0] = sin
T1[0, 2] = -cx # 平移矩陣
T1[1, 2] = -cy
T2[0, 2] = cx # 平移矩陣
T2[1, 2] = cy
S[0, 0] = S[1, 1] = scale # 縮放矩陣
M = (T2 @ S @ R @ T1) # 注意左乘順序,對應,平移-》旋轉-》縮放-》平移
# M[:2]等價於cv2.getRotationMatrix2D(center=(cx, cy), angle=rotate, scale=scale)
img = cv2.warpAffine(src, M[:2], (w, h), borderValue=(114, 114, 114))
img=np.concatenate((src,img),axis=1)
cv2.imwrite('affine.jpg', img)
#再加上shear
SH[0, 1] = SH[1, 0] = math.tan(shear / 180 * np.pi) # 兩個方向
M = (T2 @ S @ SH @ T1)
img = cv2.warpAffine(src, M[:2], (w, h), borderValue=(114, 114, 114))
#bboxs坐標轉換
#srcboxs [n,5]
# M矩陣用於列向量相乘,這里需要用轉置處理所有坐標
n=srcboxs.shape[0]
xy = np.ones((n * 4, 3))#齊次坐標
xy[:,:2]=srcboxs[:,[1,2,3,2,3,4,1,4]].reshape((n*4,2)) #順時針xy,xy,xy,xy坐標
transbox=(xy@M.T)[:,:2].reshape((n,8)).round().astype(np.int)
for idx,box in enumerate(transbox):
s = f'c{srcboxs[idx,0]}'
cv2.line(img,(box[0], box[1]),(box[2], box[3]),color=colors[1],thickness=lw)
cv2.line(img, (box[2], box[3]), (box[4], box[5]), color=colors[1], thickness=lw)
cv2.line(img, (box[4], box[5]), (box[6], box[7]), color=colors[1], thickness=lw)
cv2.line(img, (box[6], box[7]), (box[0], box[1]), color=colors[1], thickness=lw)
cv2.putText(img, s, (box[0], box[1] - 2), cv2.FONT_HERSHEY_COMPLEX, 1.0, color=colors[1], thickness=lw)
img = np.concatenate((src, img), axis=1)
cv2.imwrite('shrear.jpg',img)
#透視變換
#src=cv2.imread('../examples/test.png')
P,RX,RY=np.eye(3),np.eye(3),np.eye(3)
k=0.9
def get_one_z(a,b,c):#z1,z2 get z (0~1)
z1 = (-b + math.sqrt(b ** 2 - 4 * a * c)) / (2 * a)
z2 = (-b - math.sqrt(b ** 2 - 4 * a * c)) / (2 * a)
if z1>0 and z1<1:
return z1
else:
return z2
# 繞x軸旋轉
zx=get_one_z(1+w**2,-2,1-(k*w)**2)#一元二次方程求解
#ax=math.atan((1-zx)/(w*zx))
ax=math.asin((1-zx)/(k*w))
cosx, sinx= math.cos(ax), math.sin(ax)
RX[1, 1] = RX[2, 2] = cosx
RX[1, 2] = -sinx
RX[2, 1] = sinx
img=cv2.warpPerspective(src,RX,(w,h))
cv2.imwrite('perspective_rx.jpg', img)
# 圖像中心雙軸旋轉
zy = get_one_z(1 + h ** 2, -2, 1 - (k * h) ** 2) # 一元二次方程求解
ay = math.atan((1 - zy) / (h * zy))
cosy, siny = math.cos(ay), math.sin(ay)
RY[0, 0] = RX[2, 2] = cosy
RY[0, 2] = siny
RY[2, 0] = -siny
P=RX@RY
print(P)
M=T2@P@T1
img = cv2.warpPerspective(src, M, (w, h))
xy=xy @ M.T
transbox = (xy[:, :2] / xy[:, 2:3]).reshape(n, 8).round().astype(np.int)
for idx, box in enumerate(transbox):
s = f'c{srcboxs[idx, 0]}'
cv2.line(img, (box[0], box[1]), (box[2], box[3]), color=colors[1], thickness=lw)
cv2.line(img, (box[2], box[3]), (box[4], box[5]), color=colors[1], thickness=lw)
cv2.line(img, (box[4], box[5]), (box[6], box[7]), color=colors[1], thickness=lw)
cv2.line(img, (box[6], box[7]), (box[0], box[1]), color=colors[1], thickness=lw)
cv2.putText(img, s, (box[0], box[1] - 2), cv2.FONT_HERSHEY_COMPLEX, 1.0, color=colors[1], thickness=lw)
img = np.concatenate((src, img), axis=1)
cv2.imwrite('perspective.jpg',img)
其中繞x,y軸的角度范圍確定方式如下圖,假設黑色垂直實線為視平面,繞x軸旋轉alpha角度,空間點(0,y,z)透視后在視平面位置(0,y/z,1)處,即(0,0,1)~(0,y, z)段透視到視平面(0,0,1)~(0,y/z,1)段。我期望圖像透視后整個y方向還占k比率范圍。則有(1-z)**2+(hz)**2=(kh)**2,解得z后就可以求alpha大小

透視變換前后


這樣就可以用基本操作設計透視矩陣。
k比較大時,如0.8,0.9,P矩陣如下。


與直接設置M[2,0],M[2,1]數量級接近了,設置k比直接設置random_perspective的perspective參數更容易調節透視變換效果。
不過實際圖像增強中不會去使用旋轉、透視等,因為若這樣做原始的box變換后是傾斜的不規則的,無法獲取用於訓練的外接矩形框。
