OpenCV warpAffine做图像旋转变换90度黑边问题

OpenCV提供两种图像旋转函数,一个是cv2::rotate,只提供90,180,270三种角度的旋转。另一个是使用仿射变换的方式,实现任意角度的变换。
为了通用,我们都使用仿射变换的方式进行图像旋转操作。然而,这里就遇到一个warpAffine函数的一个坑。我们先上代码,如下:

import cv2
import plotly.express as px

def img_show(img, bgr_mode=True):
	if bgr_mode:
		px.imshow(img[...,::-1]).show()
	else:
		px.imshow(img).show()

# Reading the image
testimg = "girl02.jpg"
image = cv2.imread(testimg)

# Extracting height and width from
# image shape
height, width = image.shape[:2]
print("image shape: ", image.shape)

# get the center coordinates of the
# image to create the 2D rotation
# matrix
center = (width/2, height/2)

# using cv2.getRotationMatrix2D()
# to get the rotation matrix
rotate_matrix = cv2.getRotationMatrix2D(center=center, angle=90, scale=1)
print(rotate_matrix)

bound_w = height * np.abs(rotate_matrix[0,1]) + width * np.abs(rotate_matrix[0,0])
bound_h = height * np.abs(rotate_matrix[0,0]) + width * np.abs(rotate_matrix[0,1])
bound_w = math.ceil(round(bound_w, 10))
bound_h = math.ceil(round(bound_h, 10))
print(f"w:{bound_w}, h:{bound_h}")

rotate_matrix[0, 2] += bound_w / 2 - center[0]
rotate_matrix[1, 2] += bound_h / 2 - center[1]

# rotate_matrix[1,2] += height/2
# rotate the image using cv2.warpAffine
# 90 degree anticlockwise
rotated_image = cv2.warpAffine(src=image, M=rotate_matrix, dsize=(bound_w, bound_h), borderMode=cv2.BORDER_CONSTANT, borderValue=(128, 128, 128))

img_show(rotated_image)

输出:

image shape: (633, 552, 3)
[[ 6.123234e-17 1.000000e+00 -4.050000e+01]
 [-1.000000e+00 6.123234e-17 5.925000e+02]]
w:633, h:552

OpenCV warpAffine做图像旋转变换90度黑边问题_第1张图片

如下为原图:
OpenCV warpAffine做图像旋转变换90度黑边问题_第2张图片

看起来似乎没有什么问题对吧。现在我们把图片左上角放大看一下,图片的顶部存在一个像素宽度黑边。

OpenCV warpAffine做图像旋转变换90度黑边问题_第3张图片

检查代码,似乎每一步都没有问题,按理论应该是刚好对齐,不应该出现黑边。如果我们把旋转矩阵的 M 1 , 2 M_{1,2} M1,2系数减小1,结果就刚刚好,黑边就不见了。如果旋转是180度,左边和上边会同时出现黑边,如果旋转270度,左边会出现黑边。

  1. 一开始我们怀疑是OpenCV的计算精度问题,所以自己动手实现了旋转矩阵及其计算过程,如下:
# 创建输出图像

import numpy as np

testimg = "girl01.jpg"

# Reading the image
src = cv2.imread(testimg)

degree = 90
d1 = np.pi / 180.0
M = np.zeros((2, 3), dtype=np.float32)
h, w, _ = src.shape
print("old image size (wxh)=", (w,h))

alpha = np.cos(d1*degree).round(15)
beta = np.sin(d1*degree).round(15)
M[0, 0] = alpha
M[1, 1] = alpha
M[0, 1] = beta
M[1, 0] = -beta
cx = w / 2
cy = h / 2
tx = (1 - alpha) * cx - beta * cy
ty = beta * cx + (1 - alpha) * cy
M[0, 2] = tx
M[1, 2] = ty

# change with full size
bound_w = int(h * np.abs(beta) + w * np.abs(alpha))
bound_h = int(h * np.abs(alpha) + w * np.abs(beta))
M[0, 2] += bound_w / 2 - cx
M[1, 2] += bound_h / 2 - cy

# M = M.round(15)
print(M)
print("new image size (wxh)=", (bound_w, bound_h))

dst = np.zeros((bound_w, bound_h))

for x in range(w):
	for y in range(h):
		print("old_loc:", x, y)
		loc_old = np.array([y, x, 1])
		loc_new = np.dot(M, loc_old)
		print("new_loc:", loc_new)
		break
	break

输出:

old image size (wxh)= (1024, 1536)
[[ 0.000e+00 1.000e+00 0.000e+00]
 [-1.000e+00 0.000e+00 1.024e+03]]
new image size (wxh)= (1536, 1024)
old_loc: 0 0 new_loc: [ 0. 1024.]

我们发现源图像中坐标为(0,0)的点,在输出图像中的坐标为(0,1024),问题就来了,我们的输出图像高度是1024,最底部的一行像素的y轴坐标为1023,程序输出却是1024,以此类推,所有点的y坐标都被多算了1,所以在输出图片的第0行变成了空行,使用黑色像素填充后就是一条黑边。但是从头到位,我们的计算都是按照理论公式计算,程序并没有BUG,为什么的到的结果和想象的不一样呢?

下面分析一下原因。
我们看一下旋转角度为90度时的旋转矩阵及其计算过程:
按opencv官方给出的旋转矩阵计算公式:
OpenCV warpAffine做图像旋转变换90度黑边问题_第4张图片

计算90度的旋转矩阵:
( 0 1 0 − 1 0 w ) \begin{pmatrix} 0&1&0\\ -1&0&w\\ \end{pmatrix} (01100w)
其中, w w w为图像的宽度

我们代入公式,得到

( x ∗ y ∗ ) = ( 0 1 0 − 1 0 w ) ∗ ( x y 1 ) = ( y w − x ) \begin{pmatrix} x^*\\ y^*\\ \end{pmatrix} =\begin{pmatrix} 0&1&0\\ -1&0&w\\ \end{pmatrix}* \begin{pmatrix} x\\y\\1 \end{pmatrix} =\begin{pmatrix} y\\ w-x\\ \end{pmatrix} (xy)=(01100w)xy1=(ywx)
到目前为止看起来似乎没什么问题,其实问题就出在这个 w − z w-z wz身上。

举个下面的例子来说明,在实数轴区间[0,10]上,该区间长度为10,一个点x距离0点的距离为x,距离另一端10的距离为 10 − x 10-x 10x,区间的中心点为 10 2 \frac{10}{2} 210,没问题。但是如果在有10个点的离散数轴上,x距离0点的距离仍然为x,但是距离另一端的距离就是距离编号为9的那个点的距离,就不再是 10 − x 10-x 10x了,而是 9 − x 9-x 9x,区间的中心点为 9 2 \frac{9}{2} 29 ,即第4个点和第5个点的中间,而不是第五个点。
因此,我们实际计算图像中心的时候w要减1。
下面我们推广到更一般的情况看一下,OpenCV给出的旋转矩阵是以源图像自身的图像中心计算的的,如果我们不想让输出图像被裁切掉一部分,那么需要在第三列系数上增加一个补偿值。通常最终计算结果如下:
[ α β ( 1 − α ) C x − β C y + Δ C x − β α β C x + ( 1 − α ) C y + Δ C y ] \begin{bmatrix} \alpha & \beta & (1-\alpha)C_x-\beta C_y + \Delta C_x\\ -\beta & \alpha & \beta C_x + (1-\alpha) C_y + \Delta C_y \end{bmatrix} [αββα(1α)CxβCy+ΔCxβCx+(1α)Cy+ΔCy]
其中,
Δ C x = 1 2 ( a b s ( α ) w + a b s ( β ) h ) − C x \Delta C_x = \frac{1}{2} (abs(\alpha) w+abs(\beta) h) - C_x ΔCx=21(abs(α)w+abs(β)h)Cx
Δ C y = 1 2 ( a b s ( α ) h + a b s ( β ) w ) − C y \Delta C_y = \frac{1}{2} (abs(\alpha) h+abs(\beta) w) - C_y ΔCy=21(abs(α)h+abs(β)w)Cy
C x = w 2 C_x = \frac{w}{2} Cx=2w
C y = h 2 C_y = \frac{h}{2} Cy=2h
代入旋转矩阵计算得到:
[ α β 1 2 ( a b s ( α ) w + a b s ( β ) h ) − 1 2 ( α w + β h ) − β α 1 2 ( a b s ( α ) h + a b s ( β ) w ) − 1 2 ( α h − β w ) ] \begin{bmatrix} \alpha & \beta & \frac{1}{2} (abs(\alpha) w+abs(\beta) h) - \frac{1}{2} (\alpha w + \beta h)\\ -\beta & \alpha & \frac{1}{2} (abs(\alpha) h+abs(\beta) w) - \frac{1}{2}(\alpha h - \beta w) \end{bmatrix} [αββα21(abs(α)w+abs(β)h)21(αw+βh)21(abs(α)h+abs(β)w)21(αhβw)]

这里的中心坐标点计算是直接拿图像宽度除以2。如果我们仔细考虑一下离散域的中心点算法,使用如下方式计算图像旋转中心,得到结果如下:
令:
C x = w − 1 2 , C y = h − 1 2 C_x = \frac{w-1}{2}, C_y = \frac{h-1}{2} Cx=2w1Cy=2h1
Δ C x = 1 2 ( a b s ( α ) w + a b s ( β ) h − 1 ) − C x \Delta C_x = \frac{1}{2} (abs(\alpha) w+abs(\beta) h - 1) - C_x ΔCx=21(abs(α)w+abs(β)h1)Cx
Δ C y = 1 2 ( a b s ( α ) h + a b s ( β ) w − 1 ) − C y \Delta C_y = \frac{1}{2} (abs(\alpha) h+abs(\beta) w - 1) - C_y ΔCy=21(abs(α)h+abs(β)w1)Cy

得到旋转矩阵为:
[ α β 1 2 ( a b s ( α ) w + a b s ( β ) h ) − 1 2 ( α w + β h ) + α 2 + β 2 − 1 2 − β α 1 2 ( a b s ( α ) h + a b s ( β ) w ) − 1 2 ( α h − β w ) + α 2 − β 2 − 1 2 ] \begin{bmatrix} \alpha & \beta & \frac{1}{2} (abs(\alpha) w+abs(\beta) h) - \frac{1}{2} (\alpha w + \beta h) + \frac{\alpha}{2} + \frac{\beta}{2} - \frac{1}{2}\\ -\beta & \alpha & \frac{1}{2} (abs(\alpha) h+abs(\beta) w) - \frac{1}{2}(\alpha h - \beta w) +\frac{\alpha}{2} - \frac{\beta}{2} - \frac{1}{2} \end{bmatrix} [αββα21(abs(α)w+abs(β)h)21(αw+βh)+2α+2β2121(abs(α)h+abs(β)w)21(αhβw)+2α2β21]

其中尾部的 α 2 + β 2 − 1 2 \frac{\alpha}{2} + \frac{\beta}{2} - \frac{1}{2} 2α+2β21 α 2 − β 2 − 1 2 \frac{\alpha}{2} - \frac{\beta}{2} - \frac{1}{2} 2α2β21 分别为x轴和y轴上,使用不同中心点计算方法产生的误差项。90度时刚好为[0,-1], 180度时为[-1,-1],270度时为[-1,0]

因此修改代码如下:

testimg = "girl02.jpg"

# Reading the image
image = cv2.imread(testimg)

# Extracting height and width from
# image shape
height, width = image.shape[:2]
print("old image size (wxh)=", (width, height))

# get the center coordinates of the
# image to create the 2D rotation
# matrix
center = ((width-1)/2, (height-1)/2)

# using cv2.getRotationMatrix2D()
# to get the rotation matrix
rotate_matrix = cv2.getRotationMatrix2D(center=center, angle=90, scale=1)
print(rotate_matrix)

bound_w = height * np.abs(rotate_matrix[0,1]) + width * np.abs(rotate_matrix[0,0])
bound_h = height * np.abs(rotate_matrix[0,0]) + width * np.abs(rotate_matrix[0,1])
bound_w = int(round(bound_w, 10))
bound_h = int(round(bound_h, 10))
print("new image size (wxh)=", (bound_w, bound_h))

rotate_matrix[0, 2] += (bound_w-1) / 2 - center[0]
rotate_matrix[1, 2] += (bound_h-1) / 2 - center[1]

# rotate_matrix[1,2] += height/2
# rotate the image using cv2.warpAffine
# 90 degree anticlockwise
rotated_image = cv2.warpAffine(src=image, M=rotate_matrix, dsize=(bound_w, bound_h), borderMode=cv2.BORDER_CONSTANT, borderValue=(128, 128, 128))

img_show(rotated_image)

输出:

old image size (wxh)= (552, 633)
[[ 6.123234e-17 1.000000e+00 -4.050000e+01]
 [-1.000000e+00 6.123234e-17 5.915000e+02]]
new image size (wxh)= (633, 552)

OpenCV warpAffine做图像旋转变换90度黑边问题_第5张图片

一切正常。

你可能感兴趣的:(python,图像处理,opencv,计算机视觉)