作者 |Robin White
编译 |Flin
起源 |towardsdatascience
我最喜爱的 YouTuber 之一,CodeBullet,已经尝试创立一个乒乓球 AI 来统治所有人。可悲的是,他遇到了麻烦,不是因为他没有能力,而是我认为他过后的教训对计算机视觉没有太大影响。他相对是个好笑的人,如果你思考浏览这篇文章的其余部分,我强烈建议你观看他的视频。同样,他是个蠢才。在这里看他的视频。
- https://www.youtube.com/watch…
这仿佛是一个十分乏味且简略的工作,所以我也想尝试一下。在这篇文章中,我将概述一些我思考过的因素,如果你心愿在任何相似的我的项目上工作,这些因素可能会有所帮忙,并且我想我会尝试其中的一些其余工作,因而,如果你喜爱这种类型的事件,能够关注我。
应用计算机视觉的益处是,我能够应用曾经构建的游戏并解决图像。话虽如此,咱们将应用与 ponggame.org 上应用的那个与 CodeBullet 雷同的游戏版本。它还具备 2 人模式,因而我能够与本人的 AI 反抗;我做到了,这的确很难……
- https://www.ponggame.org/
捕获屏幕
第一件事就是捕获屏幕。我想确保我的帧速率尽可能快,为此我发现 MSS 是一个很棒的 python 包。有了这个,我很容易达到 60 帧 / 秒的最高速度,与 PIL 相比,我只能失去大概 20 帧每秒。它以 numpy 数组的模式返回。
- MSS:https://pypi.org/project/mss/
Paddle detection
为了简略起见,咱们须要定义 paddle 的地位。这能够用几种不同的办法来实现,但我认为最显著的是对每个 Paddle 的区域进行遮罩,而后运行连贯的组件来找到 Paddle 对象。上面是一段代码:
def get_objects_in_masked_region(img, vertices, connectivity = 8):
''':return connected components with stats in masked region
[0] retval number of total labels 0 is background
[1] labels image
[2] stats[0] leftmostx, [1] topmosty, [2] horizontal size, [3] vertical size, [4] area
[3] centroids
'''
mask = np.zeros_like(img)
# fill the mask
cv2.fillPoly(mask, [vertices], 255)
# now only show the area that is the mask
mask = cv2.bitwise_and(img, mask)
conn = cv2.connectedComponentsWithStats(mask, connectivity, cv2.CV_16U)
return conn
在下面,“vertices”只是定义遮罩区域的坐标列表。一旦在每个区域内有了对象,我就能够失去它们的质心地位或边界框。须要留神的一点是 OpenCV 将背景作为任何连贯的组件列表中的第 0 个对象,因而在本例中,我总是获取第二大的对象。后果如下——左边绿色质心的球拍是玩家 / 行将成为人工智能管制的球拍。
挪动 paddle
当初咱们有了输入,咱们须要一个输出。为此,我求助于一个有用的包和其他人的代码(http://stackoverflow.com/ques…)。
它应用 ctypes 来模仿键盘按下,在这种状况下,游戏是用“k”和“m”键来玩的。我这里有扫描码(http://www.gamespp.com/direct…)。在测试了它只是随机高低挪动后,咱们就能够开始跟踪了。
乒乓球检测
下一步是辨认并跟踪乒乓球。同样,这能够用几种办法来解决——其中一种可能是通过应用模板进行对象检测,然而,我再次应用了连贯的组件和对象属性,即乒乓球的区域,因为它是惟一具备尺寸的对象。
我晓得每当乒乓球穿过或碰到其余红色物体时,我都会遇到问题,但我也认为只有我能在大多数工夫里追踪到它,这所有都没问题。毕竟,它是直线运动的。如果你看上面的视频,你会看到标记乒乓球的红色圆圈是如何闪动的。这是因为它只在每 2 帧中找到一个。在 60 帧 / 秒时,这并不重要。
反弹预测的光线投射
在这一点上,咱们曾经有一个可工作的人工智能。如果咱们只是挪动球员的球拍,使其处于与乒乓球雷同的 y 轴地位,它的成果相当不错。然而,当乒乓球失去良好的反弹时,它的确会遇到问题。球拍太慢了,跟不上,须要预测乒乓球的地位,而不是仅仅挪动到以后的地位。这曾经在下面的剪辑中实现了,上面是两种办法的比拟。
差异并不大,但如果抉择了正确的人工智能,这相对是一场更稳固的胜利。为此,我首先为乒乓球创立了一个地位列表。为了偏心起见,我把这个列表的长度管制在 5 个,基本上能够做到。列表不要太长,否则要花更长的工夫能力发现它扭转了方向。在失去地位列表后,我应用简略的矢量平均法来平滑并失去方向矢量——如绿色箭头所示。这也被标准化成一个单位向量,而后乘以一个长度以不便可视化。
投射光线只是这个的延长——使前向投影变长。而后我查看了将来的地位是否在顶部和底部区域的边界之外。如果是这样的话,它只是将地位投影回游戏区域。对于左侧和右侧,它计算出与 paddle 的 x 地位相交的地位,并将 x 和 y 地位固定到该点。这样能够确保 paddle 指向正确的地位。如果没有这一点,它通常会走得太远。上面是定义光线的代码,该光线能够预测乒乓球的将来地位:
def pong_ray(pong_pos, dir_vec, l_paddle, r_paddle, boundaries, steps = 250):
future_pts_list = []
for i in range(steps):
x_tmp = int(i * dir_vect[0] + pong_pos[0])
y_tmp = int(i * dir_vect[1] + pong_pos[1])
if y_tmp > boundaries[3]: #bottom
y_end = int(2*boundaries[3] - y_tmp)
x_end = x_tmp
elif y_tmp < boundaries[2]: #top
y_end = int(-1*y_tmp)
x_end = x_tmp
else:
y_end = y_tmp
##stop where paddle can reach
if x_tmp > r_paddle[0]: #right
x_end = int(boundaries[1])
y_end = int(pong_pos[1] + ((boundaries[1] - pong_pos[0])/dir_vec[0])*dir_vec[1])
elif x_tmp < boundaries[0]: #left
x_end = int(boundaries[0])
y_end = int(pong_pos[1] + ((boundaries[0] - pong_pos[0]) / dir_vec[0]) * dir_vec[1])
else:
x_end = x_tmp
end_pos = (x_end, y_end)
future_pts_list.append(end_pos)
return future_pts_list
在下面,兴许不太显著的计算方法是确定 paddle 对指标的左或右地位的截距。咱们基本上是通过类似三角形来实现的,图片和方程如下所示。咱们晓得在边界中给定的 paddle 的 x 地位的截距。而后咱们能够计算出乒乓球将挪动多远,并将其增加到以后的 y 地位。
paddle 尽管看起来笔挺,但实际上有一个蜿蜒的反弹面。也就是说,如果你用球拍向两端击球,球会反弹,就像球拍有角度一样。因而,我容许球拍击中边缘,这减少了人工智能的攻击性,使乒乓球到处飞舞。
论断
只管是为这种特定的乒乓球实现而设计的,然而雷同的概念和代码也能够用于任何版本——只须要扭转一些预处理步骤。当然,另一种办法是通过强化学习或简略的 conv-net 应用机器学习,但我喜爱这种经典办法;至多在这种状况下,我不须要强壮的通用性或艰难的图像处理步骤。正如我提到的,这个版本的乒乓球是 2 人,诚实说我无奈战胜我本人的 AI…
如果你在这篇文章的任何局部提供了一些有用的信息或只是一点灵感,请关注我来理解更多。
你能够在我的 github 上找到源代码。
- https://github.com/robintwhite
原文链接:https://towardsdatascience.co…
欢送关注磐创 AI 博客站:
http://panchuang.net/
sklearn 机器学习中文官网文档:
http://sklearn123.com/
欢送关注磐创博客资源汇总站:
http://docs.panchuang.net/