少女祈祷中...

写在前面

此文档用于记录、以及介绍自己当时写的代码以及思路,另外让自己在重温一遍代码,感受一下自己当时有多蠢。此文档并没有改进打算,思路仅供参考。

项目简介

使用STM32F103C8T6+openmv+串口屏。其中

  • STM32F103C8T6。负责接收openmv的下棋指令,控制机械臂下棋,以及转发(至openmv)人下棋的位置,像串口屏发送棋盘状态。
  • openmv。识别棋盘和棋子的状态;选择最优下棋方案,并向stm32发送下棋指令;识别人故意挪动棋子,并发送复位指令。
  • 串口屏。显示棋盘状态;提供下棋按钮,向stm32发送下棋位置;显示机械臂单次操作所耗时间。
    因为我这里只有串口屏和openmv的代码,而串口屏只是一个简单的界面设计和显示,没什么可以讲的,使用这里只有openmv的程序流程图(有点不标准,见谅)。
    流程图1 流程图2

选材

  1. 棋盘棋子选材。
    颜色选择:由于题目只要求了棋子的颜色,并未指定棋盘的背景颜色,因此棋盘最好选择与黑白对比度比较强的、不容易受光照影响的颜色。我们队选择的是红色。
    材料选择:棋盘是由A4纸彩印的;棋子方面,由于我们是控制电磁铁来进行拿放棋操作,因此方便使用贴有黑白两种颜色的圆形铁片(刚好有符合题目要求的圆形贴纸,铁片大小也符合题目要求,这个是由老师给我们提供的)。
    其实我后面还看到过别人用软管管来拿放棋操作(就是气压差,用管子把棋子吸起来,自己当时完全没想到这种方案)。参考视频
  2. 摄像头选材。
    摄像头都有好几种吧,只是我们之前就是一直使用openMV,所以就只好选择openMV咯。其实也可以用树莓派或者K20,K20没怎么了解过,用树莓派的话,单独一个树莓派就可以了,也不用像我们一样STM32F1+openmv+串口屏这么麻烦,而且还有信号传输不太稳定的问题

代码分析

棋盘识别

在棋盘格子序号确定的情况下,如果你已经有了1~9号格子对应的中心坐标,那么扫描棋盘就非常方便。因此一开始就要确定9个格子的中心坐标。下面是对应代码(和比赛使用的代码有区别,并不保证能直接使用,但思路可供参考)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# 完善棋盘的九个格子的中心坐标
def colors_shape(lst_k):

global mode,sqr_roi,aa
# 从图片中找出红色大色块
blobs=img.find_blobs([threshold_R],margin=30)

# 用于确定棋盘是否旋转:1 不旋转,0旋转。
flag_G=0
if mode == 4 or mode == 5 or mode == 6:
flag_G=1
if blobs:
b=find_max(blobs) # 从红色色块中找出最大的----棋盘
sqr_roi = b.rect()
img.draw_rectangle(b.rect(),color = (255, 255, 255))
# img.draw_line(b.major_axis_line())
# 得到整个棋盘的四个角点的坐标(但未确定四个角点对应的实际位置)
lst1 = b.corners()

# 通过比较四个角点的位置,确定四个角点的实际位置
lst = [[0,0],[0,0],[0,0],[0,0]]
for i in range(0,4):
if lst1[i][0]<b.cx() and lst1[i][1]<b.cy():
tmp=[0,0]
tmp[0]=lst1[i][0]
tmp[1]=lst1[i][1]
lst[0] = tmp
elif lst1[i][0]>b.cx() and lst1[i][1]<b.cy():
tmp=[0,0]
tmp[0]=lst1[i][0]
tmp[1]=lst1[i][1]
lst[1] = tmp
elif lst1[i][0]<b.cx() and lst1[i][1]>b.cy():
tmp=[0,0]
tmp[0]=lst1[i][0]
tmp[1]=lst1[i][1]
lst[3] = tmp
elif lst1[i][0]>b.cx() and lst1[i][1]>b.cy():
tmp=[0,0]
tmp[0]=lst1[i][0]
tmp[1]=lst1[i][1]
lst[2] = tmp


# pz 为棋盘中心格子的中心坐标
pz = [b.cx(),b.cy()]
# 如果在找四个角点的实际位置出现了失误,就认为不旋转,使用不旋转推导出每个格子对应的坐标
for t in lst:
if t[0]==0 and t[1]==0:
flag_G=1

# 利用棋盘四个顶点的坐标,确定棋盘的旋转角度
if flag_G == 0:
# 顶点坐标
p0 = [lst[0][0],lst[0][1]]
p1 = [lst[1][0],lst[1][1]]
p2 = [lst[2][0],lst[2][1]]
p3 = [lst[3][0],lst[3][1]]
# print(p0,p1,p2,p3)
# 计算棋盘的旋转角度
dx1 = p3[0] - p0[0]
dy1 = p3[1] - p0[1]
dx2 = p1[0] - p0[0]
dy2 = p1[1] - p0[1]

radian = math.atan2(dy1, dx1)
tmp = math.degrees(radian)-90
# 感觉算出来的角度不准
if tmp <0 :
tmp -= 3
else:
tmp += 3
tmp = round(tmp)
aa = tmp
# 利用棋盘四个顶点以及中心格子的坐标,以及棋盘每个格子的大小,确定棋盘每个格子的中心坐标
if flag_G==1:
# 这里的45是图片中格子一半的的像素长度
lst_k[0] = [pz[0]-45,pz[1]-45]
lst_k[1] = [pz[0],pz[1]-45]
lst_k[2] = [pz[0]+45,pz[1]-45]
lst_k[3] = [pz[0]-45,pz[1]]
lst_k[4] = [pz[0],pz[1]]
lst_k[5] = [pz[0]+45,pz[1]]
lst_k[6] = [pz[0]-45,pz[1]+45]
lst_k[7] = [pz[0],pz[1]+45]
lst_k[8] = [pz[0]+45,pz[1]+45]
else:
# 这里是看棋盘向左旋转还是向右,dirt是方便后面的微调,因为感觉有的地方不太准
if p0[0]<p3[0]:
dirt = 3
else:
dirt=5

# 利用相似计算其它格子的中心坐标
hz0 = [int(p0[0]+dx1/6),int(p0[1]+dy1/6)]
hz1 = [int(p0[0]+dx1/2),int(p0[1]+dy1/2)]
hz2 = [int(p0[0]+dx1*5/6),int(p0[1]+dy1*5/6)]
lst_k[0] = [int(hz0[0]+dx2/6),int(hz0[1]+dy2/6)]
lst_k[1] = [int(hz0[0]+dx2/2),int(hz0[1]+dy2/2)]
lst_k[2] = [int(hz0[0]+dx2*5/6),int(hz0[1]+dy2*5/6)]
lst_k[3] = [int(hz1[0]+dx2/6),int(hz1[1]+dy2/6)]
lst_k[4] = [pz[0],pz[1]]
lst_k[5] = [int(hz1[0]+dx2*5/6),int(hz1[1]+dy2*5/6)]
lst_k[6] = [int(hz2[0]+dx2/6),int(hz2[1]+dy2/6)]
lst_k[7] = [int(hz2[0]+dx2/2),int(hz2[1]+dy2/2)]
lst_k[8] = [int(hz2[0]+dx2*5/6+dirt),int(hz2[1]+dy2*5/6)+dirt]

有了1~9号格子对应的中心坐标,那么扫描棋盘就很简单了,只需要分别检测中心坐标周围(比棋子的范围稍小一点)是黑色还是白色,然后更改储存棋盘棋子的列表。如果是人可能移动棋子,那么还得将现在的棋盘与下之前的棋盘相比较。代码这里就不贴了,太长了,但是我把链接放文末了。
关于下棋策略,我这里是分别判断所有情况,比较笨的方法。

对此次的感想

  • 毕竟是自己写的,当时为了结果好多地方都没思考完全,导致整个结构就很混乱。
  • 另外每个模块间的串口通讯也没设计好,收发-应答没有写好,可能也正是这个原因才导致评测的时候系统卡死(悲)。下次应该准备好收发-应答,最好多一个校验,没有应答就重发之类的。
  • 还有,注释也要写清楚,这次看之前的代码差点看不懂了。
    源码地址