想着疫情期间自己弄个项目练练手。说干就是干,这两天先是做xml格式的数据集(自己做真的是太麻烦了),由于数据量感觉不是很够,想通过图像增强来扩充数据量。在通过数据增强处理数据的时候,发现旋转挺好的,就是没人用,于是乎就有了这篇图像任你转,黑边不再现,的想法,通过艰难的三角函数推导,终于做出来了个半成品,哈哈哈。
点(x,y)通过顺时针旋转θ,变换为(x’,y’)。它是如何变换的呢?
这里用三角函数进行表示。先是x,y的由来
然后是,x’,y’的由来
这里想要用x,y来表示x’,y’,先要引入三角函数的展开式
根据展开式可以将x’,y’化简为下面的形式:
转换成我们熟悉的矩阵形式,
经过上面的矩阵相乘操作就可以得到了点旋转后的坐标了。
图像可以说是由一个个点组成的,在电脑的世界里,它们认为图像有以下两点特征:
1. 图像是数字矩阵;
2. 图像的起始点是图像的左上角第一个元素。
就像上面这个图一样。
因此想要实现图像旋转得先进行坐标轴的平移。就像下面这样
这里W,H分别表示矩阵的宽和长。将(0,0)平移到图像的中心点,通过矩阵表示就是
当然,我们在计算完成后还得把它的坐标轴给平移回来,这样求一下它的逆运算就可以了
这样把上面的连起来,就是图像旋转的变换公式了
先来个代码爽一下:
import math
import numpy as np
import cv2
def img_rotate(image, angle):
'''
:param image: 输入图像
:param angle: 旋转角度
:return:
'''
# 这里注意图像的形状是长和宽,而不是宽和长
h, w, channels = image.shape
# 角度转换
anglePi = angle * math.pi / 180.0
cosA = math.cos(anglePi)
sinA = math.sin(anglePi)
img_r = np.zeros((h, w, channels), dtype=np.uint8)
# 创建坐标轴平移矩阵
center_array = np.array([[1, 0, 0],
[0, -1, 0],
[-0.5*w, 0.5*h, 1]
])
# 创建坐标轴平移矩阵逆运算矩阵
i_center_array= np.array([[1, 0, 0],
[0, -1, 0],
[0.5*w, 0.5*h, 1]
])
# 创建旋转矩阵
rotate_array = np.array([[cosA, -sinA, 0],
[sinA, cosA, 0],
[0, 0, 1]
])
# 矩阵相乘,这里方便观看写成两个式子相乘,其实可以写成
#i_center_array.dot(rotate_array.dot(center_array))
rotate = np.dot(center_array, rotate_array)
i_rotate = np.dot(rotate, i_center_array)
for i in range(0, w):
for j in range(0, h):
# 这是矩阵乘完后的展开式,原理一样
# x = int(cosA*i-sinA*j-0.5*w*cosA+0.5*h*sinA+0.5*w)
# y = int(sinA*i+cosA*j-0.5*w*sinA-0.5*h*cosA+0.5*h)
new = np.dot(np.array([i, j, 1]), i_rotate)
# 坐标没有小数,必须取整
x = int(new[0])
y = int(new[1])
# 防止溢出
if x >= 0 and x < w and y >= 0 and y < h:
img_r[j, i] = image[y, x]
return img_r
image = cv2.imread("E:\Learn\dataset\dtd\images/bubbly/bubbly_0096.jpg")
image_r = img_rotate(image, 30)
cv2.imshow('rotate',image_r)
cv2.imshow('original',image)
cv2.waitKey()
可以看到,这里旋转后的图像被截取了四个角,并且还有黑边。因此,出现了图像旋转的改良版,原理就是对图像进行扩大,大小为旋转后四个角坐标中的最大值作为长和宽。这里就不放代码了,要不后面正主写不下了。但同样的问题是,这种办法还是有黑边,而且图像的尺寸改变了,这并不是我们想要的,特别是我们需要旋转图像进行数据增强时。那如何在不改变图像尺寸的同时还能去除黑边呢?
这里要先来一张图,看一下几何上图像旋转是啥样的。
可以看出了,这里是逆时针旋转的,旋转角度为θ(但是我上边的代码是是按照顺时针旋转写的,结果跑完就成逆时针旋转了,也不知道咋回事。所以后面我就按照逆时针旋转来写如何去除黑色的边边角角。)
也就是将下图中的四个黑色的三角形给它弄没了。
具体如何实现呢?将图像看做一个矩形,求出原矩形ABCD和旋转后的矩形A’B’C’D’的交点,如下图
以其中一个三角形BEF为例,这里有两种思路,
(1)将三角形BEF扩充成一个矩形BEGF
这样可以得到三角形BEF和三角形GEF两个全等三角形。用三角形GEF内的像素值来替代三角形BEF内的黑色像素值。
这个方法有以下缺点:
(2)同样是对三角形进行扩充,不过这回是通过B’C’线段进行镜像扩充,如下图所示
这样同样可以得到三角形BEF和三角形GEF两个全等三角形。用三角形GEF内的像素值来替代三角形BEF内的黑色像素值。这个方法与(1)相比,旋转后的三角形图像具有D’C’方向的方向特征。但是它还是有以下缺点:
重点来了,这里给大家看看通过(1)方法进行图像变换的原理和效果,这里选取不同特征的图像进行测试。(至于方法(2)我还没写出来代码,哈哈哈)
得先求出来旋转后A’B’C’D’各个线段的表达式,然后求出交点的坐标,进而确定黑色三角形的面积。
# 直线A'B':y = tanA*x + h/(2*cosA)
# 直线B'C':y = -1/tanA*x + w/(2*sinA)
# 直线C'D':y = tanA*x - h/(2*cosA)
# 直线D'A':y = -1/tanA*x - w/(2*sinA)
# 直线B'C'与 AB 交点
x_BC_AB = int(-(h/2-w/(2*sinA))*tanA) ; y_BC_AB = h//2
# 直线B'C'与 BC交点
x_BC_BC = w//2;y_BC_BC = int(-w/(2*tanA)+w/(2*sinA))
BC_AB = np.dot(np.array([x_BC_AB, y_BC_AB, 1]), i_center_array)
BC_BC = np.dot(np.array([x_BC_BC, y_BC_BC, 1]), i_center_array)
# 直线C'D'与 CD 交点
x_CD_CD = int((-h/2+h/(2*cosA))/tanA) ; y_CD_CD = -h//2
# 直线C'D'与 BC 交点
x_CD_BC = w//2;y_CD_BC = int(w/2*tanA-h/(2*cosA))
CD_CD = np.dot(np.array([x_CD_CD, y_CD_CD, 1]), i_center_array)
CD_BC = np.dot(np.array([x_CD_BC, y_CD_BC, 1]), i_center_array)
# 直线D'A'与 CD 交点
x_DA_CD = int((h / 2 - w / (2 * sinA)) * tanA); y_DA_CD = -h // 2
# 直线D'A'与 AD 交点
x_DA_AD = -w // 2; y_DA_AD = int(w / (2 * tanA) - w / (2 * sinA))
DA_CD = np.dot(np.array([x_DA_CD, y_DA_CD, 1]), i_center_array)
DA_AD = np.dot(np.array([x_DA_AD, y_DA_AD, 1]), i_center_array)
# 直线A'B'与 AB 交点
x_AB_AB = int((h / 2 - h / (2 * cosA)) / tanA); y_AB_AB = h // 2
# 直线A'B'与 AD 交点
x_AB_AD = -w // 2; y_AB_AD = int(-w / 2 * tanA + h / (2 * cosA))
AB_AB = np.dot(np.array([x_AB_AB, y_AB_AB, 1]), i_center_array)
AB_AD = np.dot(np.array([x_AB_AD, y_AB_AD, 1]), i_center_array)
这里注意一点,黑色像素的数值为0,为了方便计算,这里直接相加就可以得到想要的
先来一张路飞的图片,哈哈哈。从左到右依次是原图,正常的旋转和去黑边的旋转
可以看出来,对于这种图像,效果并不是很好。
然后来一张气泡图
这个看着就能好多了,接着换一张,这个边界就很明显了
接下来,用一张织物的图像来试一下。
可以看出来效果还是可以的,接下来就是鼓动如何去除边界了。
经过本人不断地研究,想了不同的方法,经过验证结果效果都不如这个,这个方法呢出现黑边或者亮边是由于像素的叠加导致的,因此在图像区域ROI的时候,减个1就能得到很大的改善。