导读
本人在阅读yolov5源码的时候,发现它的马赛克数据增强方法里面使用了透视变换做为数据增强的方法。
遂在查阅了相关资料,自己手动测试了相关微博的源码,形成了本篇,码字不易,如有错误欢迎批评指正。
并且利用opencv的相关接口,
将上图的扑克牌单独提取出来,如下图所示:
如何来实现这个功能呢?这个其实就涉及到了图像的一个空间变换,就需要用到我们所说的透视变换了。
透视变换原理
透视变换(Perspective Transformation) 就是将一个平面通过一个投影矩阵投影到指定平面上。
透
视
变
换
矩
阵
变
换
公
式
为
:
[
X
Y
Z
]
=
[
a
11
a
12
a
13
b
11
b
12
b
13
c
11
c
12
c
13
]
[
x
y
1
]
=
M
[
x
y
1
]
其
中
透
视
变
换
矩
阵
:
M
=
[
a
11
a
12
a
13
b
11
b
12
b
13
c
11
c
12
c
13
]
要
移
动
的
点
,
即
源
目
标
点
为
:
[
x
y
1
]
矩
阵
相
乘
后
得
到
的
点
,
即
移
动
到
的
目
标
点
为
:
[
X
Y
Z
]
=
[
a
11
∗
x
+
a
12
∗
y
+
a
13
b
11
∗
x
+
b
12
∗
y
+
b
13
c
11
∗
x
+
c
12
∗
y
+
c
13
]
\begin{aligned} 透视变换矩阵变换公式为: &\left[ \begin{matrix} X\\Y\\Z \end{matrix} \right] = \left[ \begin{matrix} a_{11}&a_{12}&a_{13}\\b_{11}&b_{12}&b_{13}\\c_{11}&c_{12}&c_{13} \end{matrix} \right] \left[ \begin{matrix} x\\y\\1 \end{matrix} \right] = M\left[ \begin{matrix} x\\y\\1 \end{matrix} \right]\\ 其中透视变换矩阵: &M=\left[ \begin{matrix} a_{11}&a_{12}&a_{13}\\b_{11}&b_{12}&b_{13}\\c_{11}&c_{12}&c_{13} \end{matrix} \right]\\ 要移动的点,即源目标点为: &\left[ \begin{matrix} x\\y\\1 \end{matrix} \right]\\ 矩阵相乘后得到的点,即移动到的目标点为: &\left[ \begin{matrix} X\\Y\\Z \end{matrix} \right] =\left[ \begin{matrix} a_{11}*x+a_{12}*y+a_{13}\\b_{11}*x+b_{12}*y+b_{13}\\c_{11}*x+c_{12}*y+c_{13} \end{matrix} \right] \end{aligned}
透视变换矩阵变换公式为:其中透视变换矩阵:要移动的点,即源目标点为:矩阵相乘后得到的点,即移动到的目标点为:⎣⎡XYZ⎦⎤=⎣⎡a11b11c11a12b12c12a13b13c13⎦⎤⎣⎡xy1⎦⎤=M⎣⎡xy1⎦⎤M=⎣⎡a11b11c11a12b12c12a13b13c13⎦⎤⎣⎡xy1⎦⎤⎣⎡XYZ⎦⎤=⎣⎡a11∗x+a12∗y+a13b11∗x+b12∗y+b13c11∗x+c12∗y+c13⎦⎤
上面的透视变换矩阵M,可以将其拆成四个部分:
- 第一部分表示线性变换,这部分矩阵主要用于图像的缩放、旋转操作,在仿射变换中我们也介绍过。
[ a 11 a 12 b 11 b 12 ] \left[ \begin{matrix} a_{11}&a_{12}\\b_{11}&b_{12}&\end{matrix} \right] [a11b11a12b12]
- 第二部分用来进行平移操作 [ a 13 b 13 ] \left[\begin{matrix}a_{13}&b_{13} \end{matrix}\right] [a13b13]
- 第三部分用来产生透视变换 [ c 11 c 12 ] \left[\begin{matrix}c_{11}&c_{12} \end{matrix}\right] [c11c12]
- 第四部分参数 c 13 c_{13} c13等于1
透视变换是一个从二维空间变换到三维空间的转换,我们最终要得到的是图像在二维平面上的投影,故除以Z, (X’,Y’)表示二维平面上图像上的点:
{
X
′
=
X
Z
=
a
11
∗
x
+
a
12
∗
y
+
a
13
c
11
∗
x
+
c
12
∗
y
+
c
13
Y
′
=
Y
Z
=
b
11
∗
x
+
b
12
∗
y
+
b
13
c
11
∗
x
+
c
12
∗
y
+
c
13
\left\{ \begin{matrix} X'=\frac{X}{Z}=\frac{a_{11}*x+a_{12}*y+a_{13}}{c_{11}*x+c_{12}*y+c_{13}}\\ \\ Y'=\frac{Y}{Z}=\frac{b_{11}*x+b_{12}*y+b_{13}}{c_{11}*x+c_{12}*y+c_{13}} \\ \end{matrix} \right.
⎩⎨⎧X′=ZX=c11∗x+c12∗y+c13a11∗x+a12∗y+a13Y′=ZY=c11∗x+c12∗y+c13b11∗x+b12∗y+b13
令
a
33
=
1
a_{33}=1
a33=1,展开上面公式,得到一个点的情况
{
a
11
x
+
a
12
y
+
a
13
−
c
11
x
X
′
−
c
12
y
X
′
=
X
′
b
11
x
+
b
12
y
+
b
13
−
c
11
x
Y
′
−
c
12
y
Y
′
=
Y
′
\left\{ \begin{matrix} a_{11}x+a_{12}y+a_{13}-c_{11}xX'-c_{12}yX'=X'\\ \\ b_{11}x+b_{12}y+b_{13}-c_{11}xY'-c_{12}yY'=Y' \\ \end{matrix} \right.
⎩⎨⎧a11x+a12y+a13−c11xX′−c12yX′=X′b11x+b12y+b13−c11xY′−c12yY′=Y′
4个点可以得到8个方程,即可解出A:
M
=
[
a
11
a
12
a
13
b
11
b
12
b
13
c
11
c
12
c
13
]
M=\left[ \begin{matrix} a_{11}&a_{12}&a_{13}\\b_{11}&b_{12}&b_{13}\\c_{11}&c_{12}&c_{13} \end{matrix} \right]
M=⎣⎡a11b11c11a12b12c12a13b13c13⎦⎤
接下来我们看一个例子,原始图像的四个点的坐标分别为
(
0
,
0
)
、
(
1
,
0
)
、
(
1
,
1
)
、
(
0
,
1
)
(0,0)、(1,0)、(1,1)、(0,1)
(0,0)、(1,0)、(1,1)、(0,1)与之对应变换后的四个点坐标分别为
(
x
1
,
y
1
)
、
(
x
2
,
y
2
)
、
(
x
3
,
y
3
)
、
(
x
4
,
y
4
)
(x_1,y_1)、(x_2,y_2)、(x_3,y_3)、(x_4,y_4)
(x1,y1)、(x2,y2)、(x3,y3)、(x4,y4),根据上面的公式和对应的四个点坐标可得下面的方程式
x
1
=
a
13
y
1
=
b
13
x
2
=
a
11
+
a
13
−
c
11
∗
x
2
y
2
=
b
11
+
b
13
−
c
11
∗
y
2
x
3
=
a
11
+
a
12
+
a
13
−
c
11
∗
x
3
−
c
12
∗
x
3
y
3
=
b
11
+
b
12
+
c
13
−
c
11
∗
y
3
−
c
12
∗
y
3
x
4
=
a
12
+
a
13
−
c
12
∗
x
4
y
4
=
b
12
+
b
13
−
c
12
∗
y
4
\begin{aligned} x_1 = a_{13}&\\ y _1=b_{13}&\\ x _2 =a _{11}& +a _{13} −c_{11} ∗x_{2}\\ y _2 =b _{11} &+b_{13} −c_{11}∗y_2\\ x _3 =a_{11} &+a_{12} +a_{13}−c_{11} ∗x_3 −c_{12} ∗x_3\\ y _3 =b_{11}& +b_{12} +c_{13} −c_{11}∗y_3 −c_{12}∗y_3\\ x _4 =a_{12} &+a_{13} −c_{12} ∗x_4\\ y _4 =b_{12} &+b_{13} −c_{12}∗y_4\\ \end{aligned}
x1=a13y1=b13x2=a11y2=b11x3=a11y3=b11x4=a12y4=b12+a13−c11∗x2+b13−c11∗y2+a12+a13−c11∗x3−c12∗x3+b12+c13−c11∗y3−c12∗y3+a13−c12∗x4+b13−c12∗y4
通过上面的8个方程,我们可以解出8个参数求出透视变换矩阵,最后我们通过opencv的warpPerspective方法利用透视变换矩阵来实现透视变换,接下来我们通过结合一个实例来具体运用一下。
透视变换实例讲解
这里我们主要通过opencv来实现上面介绍的那个功能
- 读取图像
#读取图像
img = cv2.imread("poker.jpg")
cv2.imshow("img",cv2.resize(img,(int(0.5*img.shape[1]),int(0.5*img.shape[0]))))
cv2.waitKey(0)
#将原图转为灰度图
gray_img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
- Canny边缘检测
C a n n y 函 数 参 数 解 析 : i m a g e : 输 入 图 像 数 组 t h r e s h o l d 1 : 最 低 的 阈 值 t h r e s h o l d 2 : 最 高 的 阈 值 e d g e s : 输 出 的 边 缘 图 像 , 单 通 道 8 位 图 像 a p e r t u r e S i z e : S o b e l 算 子 的 大 小 L 2 g r a d i e n t : 布 尔 值 , 如 果 为 真 , 则 使 用 更 精 确 的 L 2 范 数 进 行 计 算 , 否 则 使 用 L 1 范 数 \begin{aligned} Canny函数参数解析:\\& image:输入图像数组\\& threshold1:最低的阈值\\& threshold2:最高的阈值\\& edges:输出的边缘图像,单通道8位图像\\& apertureSize:Sobel算子的大小\\& L2gradient:布尔值,如果为真,则使用更精确的L2范数进行计算,否则使用L1范数 \end{aligned} Canny函数参数解析:image:输入图像数组threshold1:最低的阈值threshold2:最高的阈值edges:输出的边缘图像,单通道8位图像apertureSize:Sobel算子的大小L2gradient:布尔值,如果为真,则使用更精确的L2范数进行计算,否则使用L1范数
#Canny边缘检测
canny_img = cv2.Canny(gray_img,180,200,3)
#显示边缘检测后的图像
cv2.imshow("canny_img",cv2.resize(canny_img,(int(0.5*canny_img.shape[1]),int(0.5*canny_img.shape[0]))))
cv2.waitKey(0)
- 霍夫直线检测
H o u g h L i n e s P 函 数 参 数 解 析 : i m a g e : 经 过 C a n n y 边 缘 检 测 后 的 输 出 图 像 r h o : 极 坐 标 的 半 径 r 以 像 素 值 为 单 位 的 分 辨 率 , 一 般 使 用 1 像 素 t h e t a : 极 坐 标 的 极 角 θ 以 弧 度 为 单 位 的 分 辨 率 , 一 般 使 用 1 度 t h r e s h o l d : 检 测 一 条 直 线 所 需 最 少 的 曲 线 交 点 l i n e s : 存 储 检 测 到 的 直 线 , 包 含 直 线 的 起 点 和 终 点 坐 标 m i n L i n e L e n g t h : 组 成 一 条 直 线 的 最 少 点 的 数 量 , 点 数 量 不 足 的 直 线 将 被 抛 弃 m a x L i n e G a p : 在 一 条 直 线 上 的 点 的 最 大 距 离 \begin{aligned} HoughLinesP函数参数解析:\\& image:经过Canny边缘检测后的输出图像\\& rho:极坐标的半径r以像素值为单位的分辨率,一般使用1像素\\& theta:极坐标的极角\theta以弧度为单位的分辨率,一般使用1度\\& threshold:检测一条直线所需最少的曲线交点\\& lines:存储检测到的直线,包含直线的起点和终点坐标\\& minLineLength:组成一条直线的最少点的数量,点数量不足的直线将被抛弃\\& maxLineGap:在一条直线上的点的最大距离\\& \end{aligned} HoughLinesP函数参数解析:image:经过Canny边缘检测后的输出图像rho:极坐标的半径r以像素值为单位的分辨率,一般使用1像素theta:极坐标的极角θ以弧度为单位的分辨率,一般使用1度threshold:检测一条直线所需最少的曲线交点lines:存储检测到的直线,包含直线的起点和终点坐标minLineLength:组成一条直线的最少点的数量,点数量不足的直线将被抛弃maxLineGap:在一条直线上的点的最大距离
def draw_line(img,lines):
# 绘制直线
for line_points in lines:
cv2.line(img,(line_points[0][0],line_points[0][1]),(line_points[0][2],line_points[0][3]),
(0,255,0),2,8,0)
cv2.imshow("line_img", cv2.resize(img,(int(0.5*img.shape[1]),int(0.5*img.shape[0]))))
cv2.waitKey(0)
# #Hough直线检测
lines = cv2.HoughLinesP(canny_img,1,np.pi/180,70,minLineLength=150,maxLineGap=30)[0:4]
#基于边缘检测的图像来检测直线
draw_line(img,lines)
- 计算顶点坐标
通过直线两个端点的坐标来计算直线的交点坐标,找出扑克牌的四个顶点位置
#计算四条直线的交点作为顶点坐标
def computer_intersect_point(lines):
def get_line_k_b(line_point):
"""计算直线的斜率和截距
:param line_point: 直线的坐标点
:return:
"""
#获取直线的两点坐标
x1,y1,x2,y2 = line_point[0]
#计算直线的斜率和截距
k = (y1 - y2)/(x1 - x2)
b = y2 - x2 * (y1 - y2)/(x1 - x2)
return k,b
#用来存放直线的交点坐标
line_intersect = []
for i in range(len(lines)):
k1,b1 = get_line_k_b(lines[i])
for j in range(i+1,len(lines)):
k2,b2 = get_line_k_b(lines[j])
#计算交点坐标
x = (b2 - b1) / (k1 - k2)
y = k1 * (b2 - b1)/(k1 -k2) + b1
if x > 0 and y > 0:
line_intersect.append((int(np.round(x)),int(np.round(y))))
return line_intersect
def draw_point(img,points):
for position in points:
cv2.circle(img,position,5,(0,0,255),-1)
cv2.imshow("draw_point",cv2.resize(img,(int(0.5*img.shape[1]),int(0.5*img.shape[0]))))
cv2.waitKey(0)
#计算直线的交点坐标
line_intersect = computer_intersect_point(lines)
#绘制交点坐标的位置
draw_point(img,line_intersect)
- 对顶点坐标进行排序
在计算透视变换矩阵之前我们需要对元素图像的坐标与变换后图像的坐标一一对应,按照左->上->右->下的顺序
def order_point(points):
"""对交点坐标进行排序
:param points:
:return:
"""
points_array = np.array(points)
#对x的大小进行排序
x_sort = np.argsort(points_array[:,0])
#对y的大小进行排序
y_sort = np.argsort(points_array[:,1])
#获取最左边的顶点坐标
left_point = points_array[x_sort[0]]
#获取最右边的顶点坐标
right_point = points_array[x_sort[-1]]
#获取最上边的顶点坐标
top_point = points_array[y_sort[0]]
#获取最下边的顶点坐标
bottom_point = points_array[y_sort[-1]]
return np.array([left_point,top_point,right_point,bottom_point],dtype=np.float32)
def target_vertax_point(clockwise_point):
#计算顶点的宽度(取最大宽度)
w1 = np.linalg.norm(clockwise_point[0]-clockwise_point[1])
w2 = np.linalg.norm(clockwise_point[2]-clockwise_point[3])
w = w1 if w1 > w2 else w2
#计算顶点的高度(取最大高度)
h1 = np.linalg.norm(clockwise_point[1]-clockwise_point[2])
h2 = np.linalg.norm(clockwise_point[3]-clockwise_point[0])
h = h1 if h1 > h2 else h2
#将宽和高转换为整数
w = int(round(w))
h = int(round(h))
#计算变换后目标的顶点坐标
top_left = [0,0]
top_right = [w,0]
bottom_right = [w,h]
bottom_left = [0,h]
return np.array([top_left,top_right,bottom_right,bottom_left],dtype=np.float32)
#对原始图像的交点坐标进行排序
clockwise_point = order_point(line_intersect)
#获取变换后坐标的位置
target_clockwise_point = target_vertax_point(clockwise_point)
- 计算变换矩阵进行透视变换
#计算变换矩阵
matrix = cv2.getPerspectiveTransform(clockwise_point,target_clockwise_point)
print(matrix)
#计算透视变换后的图片
perspective_img = cv2.warpPerspective(img,matrix,(target_clockwise_point[2][0],target_clockwise_point[2][1]))
cv2.imshow("perspective_img",cv2.resize(perspective_img,(int(0.5*perspective_img.shape[1]),int(0.5*perspective_img.shape[0]))))
cv2.waitKey(0)
参考文档:
-
https://xiulian.blog.csdn.net/article/details/104281693
-
https://blog.csdn.net/cuixing001/article/details/80261189?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_baidulandingword-3&spm=1001.2101.3001.4242