24年电赛E题三子棋,视觉openmv代码(包括赛题的问题分析硬件搭建和视觉的困难分析以及代码的解决方案)

前言:作者是一名大一学生,第一次参加电赛,现在获奖情况未知,但是在比赛中,我也是收获良多,对视觉类的代码也有了许多自己的见解。因为硬件openmv买的是二手,可能不是特别好,所以视觉捕捉到的画面效果很差,没法正常的取识别棋子和棋盘,为此我也是苦思冥想,前前后后差不多花了四十个小时通宵不睡觉才肝出来这个代码。在写了十几个可以对干扰过滤的函数后,我的代码滤波效果比较显著,基本上可以剔除百分之99的干扰,在后面测试阶段也没有出现过一次误识别的情况。


一、视觉之外的困难与方案选择(有代码)

1、摄像头与机械臂的摆放方案

在这次比赛中,我选择的方案是将摄像头架高后面向棋盘,时摄像头捕捉到的画面与棋盘的平面是平行的,以减少摄像头畸变带来的误差影响。

关于机械臂的摆放,我们有过许多思考,最后一致认为机械臂的底座要与摄像头的相对位置固定,因为这样子会利于后续的软件编写,具体分析为:我们可将摄像头捕捉到的画面做出一个平面直角坐标系,在坐标系中可以得出棋子和棋盘的坐标信息。同时,为了摄像头可以抓取棋子,我们也必须知道机械臂在棋盘屏幕投影的坐标信息。将摄像头的支架底座与机械臂的底座固定为一个整体后,我们可以通过测量和计算机械臂的投影坐标,进而可以准确的控制机械臂。

如下图,openmv被支架固定,机械臂和支架之间通过板子连接。使得机械臂的坐标可以确定,使其无需在下棋过程中再去寻找出机械臂的坐标。

2、棋盘的选择

如上图所示,我选择的是蓝底的棋盘。在此之前,我尝试过各种不同的棋盘,赛题要求说是棋盘要单一背景颜色,为了放置意外,我将格子内外的颜色都设置为一样的,虽然会降低一点识别的成功率,但是总体来说还是无伤大雅的。

在棋盘颜色方面,混色棋盘识别效果比较好的有橙蓝,其他的例如红绿,黄蓝等效果都不太好。单色的有蓝或者绿的效果还不错。

3、视觉与机械臂的交互

我们采取了用串口通信的方式来进行单片机之间的通信。以及通过按钮来改变视觉模块的状态机标志位来实现不同功能。在比赛时,openmv会向机械臂发送棋盘各个格子的坐标信息、等待摆放的棋子的坐标信息、棋盘中的某个格子是否有棋子存在以及存在的棋子的颜色的信息。stm32芯片将坐标信息进行函数关系运算后输出pwm波至机械臂的各个舵机,从而进行下棋。

以下是发送棋盘坐标的串口通信函数代码,以16进制数的数据包发送,有一个帧头和一个帧尾,省略了串口配置过程。

def sending_data(x1,y1,x2,y2,x3,y3,x4,y4,x5,y5,x6,y6,x7,y7,x8,y8,x9,y9):
    global uart;
    data = ustruct.pack("<bbbbbbbbbbbbbbbbbbbb",
                   0xFA,x1,y1,x2,y2,x3,y3,x4,y4,x5,y5,x6,y6,x7,y7,x8,y8,x9,y9,0xDA)
    uart.write(data)

二、视觉方面遇到的问题与解决方法(有代码展示与讲解)

1、摆在棋盘外的棋子识别问题

初步的方法选择:

因为棋子摆放在棋盘两列,可自行放置棋子,为了便于识别,我们选择将黑棋放在视觉范围的左侧,白棋在右侧。棋子在识别过程中可以用的方法有用色块识别和圆形识别。我选择采用的方法是将两种识别方式进行结合。两种识别的各个参数可以根据实际情况来确定,因为棋子在视觉画面中比较小,所以x_stride和y_stride可以设置小点。而且因为棋子摆放的位置比较接近margin也要设置得小一点,避免相近的棋子色块被合并。

blacks_start=img.find_blobs([black],roi=roi_qizi1,x_stride=3,y_stride=3,pixels_threshold=30,merge=True,margin=1)
whites_start=img.find_blobs([white],roi=roi_qizi2,x_stride=3,y_stride=3,pixels_threshold=30,merge=True,margin=1)
black_circles=img.find_circles(threshold=1500,roi=roi_qizi1,x_margin=10,y_margin=10,r_margin=10, r_min=2,r_max=10,r_step=2)
white_circles=img.find_circles(threshold=1500,roi=roi_qizi2,x_margin=10,y_margin=10,r_margin=10, r_min=2,r_max=10,r_step=2)

棋子识别的函数,将色块识别与圆形识别结合

#棋子坐标确定,由颜色和形状共同确认
def qizi_zuobiao(b,w,c):#输入色块识别黑,白,圆形识别
    bqizi_xy=[]
    wqizi_xy=[]
    for x in c:
        for y in b:
            zhongzhuan=[]
            if abs(y[0]-x[0])<6 and abs(y[1]-x[1])<6:
                zhongzhuan.append(int(y[0]/2+x[0]/2))
                zhongzhuan.append(int(y[1]/2+x[1]/2))
                bqizi_xy.append(zhongzhuan)#得出滤波后的黑棋中心的坐标
        for y in w:
            zhongzhuan=[]
            if abs(y[0]-x[0])<6 and abs(y[1]-x[1])<6:
                zhongzhuan.append(int(y[0]/2+x[0]/2))
                zhongzhuan.append(int(y[1]/2+x[1]/2))
                wqizi_xy.append(zhongzhuan)#得出滤波后的白棋中心的坐标
    zong=[]
    zong.append(bqizi_xy)
    zong.append(wqizi_xy)
    return zong  #返回一个三维列表,先是黑色再白色,先上方坐标再下方坐标,先x后y

识别时遇到的干扰问题:

在识别棋子时,摄像头有时候会误将棋盘光线亮度较高,颜色较淡的区域识别为白色色块,同时也会将棋盘黑线部分误识别为黑色色块。在这样的情况下识别就会有较大的波动。同时,圆形识别也会有一定的波动。

ps:后续我了解到上述这些波动在有些组别的openmv上是没有的,可能是硬件之间的差异。

解决方案:

在最初时,我的方案时选择将色块识别和圆形识别取交集来识别出棋子,但是因为色块识别和圆形识别的成功率不是太高,我的硬件色块识别成功率为70%左右,色块识别为90%。若是取交集,则容易少识别出三四颗棋子,所以我放弃了取交集,而是取并集。

但是前文已经提及了,识别的波动很大,有很多误识别的信息,所以在取并集之前要进行滤波。

以下是棋子的滤波,这个的中心思想就是,通过取出识别到的棋子的横坐标中位数在整齐摆放的棋子之间x之差会较小,而错误识别的色块x之差较大,我们可以利用中位数来滤波掉一些错误的信息。

#棋子滤波,滤波出棋子的初始位置,用x的中位数滤波
def qizi_lvbo1(a):#输入识别到的棋子中心坐标数组
    p=[]
    z=zhongweishu(a)
    for i in a:
        if abs(i[0]-z)<10:
            p.append(i)
    return p

在识别时,为了提高识别的精确度,减少发给主机错误的信息,我写出了一个稳定计次的函数,即在每次识别出的棋子坐标只是在小范围内跳变时,就计次加一,在计次累加到一定程度后就可以向主机发送棋子的坐标信息。

#棋子稳定后发出棋子坐标
def qizi_wending(a,b):#last_heiqi_start,heiqi_start
    if abs(a[0][0]-b[0][0])<5 and abs(a[0][1]-b[0][1])<5 and abs(a[1][0]-b[1][0])<5 :
        if abs(a[1][1]-b[1][1])<5 and abs(a[2][0]-b[2][0])<5 and abs(a[2][1]-b[2][1])<5 :
            if abs(a[3][0]-b[3][0])<5 and abs(a[3][1]-b[3][1])<5 and abs(a[4][0]-b[4][0])<5 and abs(a[4][1]-b[4][1])<5 :
                return True
    else:
        return False

因为识别棋子取并集的函数较长,我就这里不做展示了,需要的朋友们可以取我的资源里面下载。

2、棋盘的识别问题

方案的初步选择与遇到的困难:

识别棋盘我选用的方法是使用了矩形识别的函数find_rects。通过openmv函数库我们可以得知这个函数可以返回识别到的矩形的四个角,从而算出棋盘内各个格子的中心点坐标。

我们需要的是整个棋盘的四个顶点坐标,但是在识别过程中会有时将棋盘中的小格识别出来,这回有许多的波动,而且因为光线和硬件问题,识别出的棋盘顶点会有些许偏差。还有除了在棋盘的区域,在画面的其他区域也会有莫名其妙的波动,这使得棋盘识别也及其不稳定。

解决方案:

在面对这么多干扰波动时,我采取了几种滤波方式共同使用。

棋盘在画面中所占的像素多少是在一个小范围内的,我们可以利用这个面积以及长宽来进行滤波。其次因为棋盘是正方形,所以对角线垂直,通过这两个特点,我写出来一个综合的滤波函数

#棋盘滤波
def qipan_lvbo(a,b):#输入r.rect(),r.corners()

    if a[2]>60:
        if a[3]>60:
            if a[2]<110:
                if a[3]<110:
                    #if abs(b[0][0]-b[2][0])>50:
                    #    if abs(b[1][1]-b[3][1])>50:
                    if abs(b[0][0]+b[1][1]-b[2][0]-b[3][1])<20:
                        return True

可这一个滤波函数还是有些许不足,在识别过程中还是偶尔会有波动,为了确保发送的棋盘数据正确,我们还要写一个同识别棋子的稳定计次的函数。

#棋盘滤波,稳定后发出棋盘坐标
def qipan_wending(a,b):#输入r.rect()与上次的这个
    if abs(a[0]-b[0])<5:
         if abs(a[1]-b[1])<5:
              if abs(a[2]-b[2])<5:
                   if abs(a[3]-b[3])<5:
                       return True

上述函数可以判断棋盘的识别过程中是否有数据跳变,倘若没有,则可以进行计次,当计次累加到给定次数时再发送棋盘信息。

3、下棋时识别棋子与棋盘位置关系的问题

在前文中,我们已经算出了棋盘的坐标位置,也有了识别棋子的方法,这时候我们可以去识别棋盘上的各个格子内有无棋子以及有什么颜色的棋子。

具体的识别思路为先用识别棋子的方法找出棋子坐标,再用这个坐标与棋盘内的九个格子坐标比对,差值在一定范围内的时候即可认定时此棋子在此棋格内。同时这一步也可去除一定噪音的干扰。

#判断棋子是在几号位置
def qizi_bianhao(a,b,c):#输入棋盘中心列表与棋子坐标列表.a为二维列表,b为三维列表,先黑后白
    i=0
    bianhao=[0,0,0,0,0,0,0,0,0]#将棋盘信息记录在列表,0表示无,1表示黑,2表示白
    for x in a:#遍历格子
        t=0
        for u in c:
            if abs(u[0]-x[0])<9 and abs(u[1]-x[1])<9:
                bianhao[i]=1
        for y in b[0]:#遍历黑色
            if abs(y[0]-x[0])<9 and abs(y[1]-x[1])<9:
                bianhao[i]=1

        for y in b[1]:
            if abs(y[0]-x[0])<9 and abs(y[1]-x[1])<9:
                bianhao[i]=2

        i=i+1
    return bianhao#一维列表返回棋盘上棋子的信息

当然在这一步的过程中也会有一些干扰,但是也与前面的滤波大差不差也就并未单独罗列出来了。

三、视觉代码的一些其他细节

主循环的编写逻辑:

我在主循环中设置了两个大状态机,状态机1识别并传输棋盘和棋子的坐标,状态机2识别并传输棋盘上所下棋子的信息。而在状态机1中又分别设置两个小状态机,由此分别识别棋子和棋盘。对于赛题前几个要求,由于不需要人机对弈,可以只使用状态机1。在人机对弈时,将棋盘与棋子坐标识别完成后即可进入状态机2来进行识别棋局了。

状态机的切换可以通过按钮实现,按钮一端与主机的GND相连,一端与openmv的引脚连接,引脚设置为上拉输入即可。这样按钮按下以下就切换一个状态机。

p_in5 = Pin("P5", Pin.IN, Pin.PULL_UP)#配置端口

#识别按钮切换状态机
    if p_in5.value()==0:
        pyb.delay(300)
        if page==1:
            page=2
        else:
            page=1

画面的调整处理:

openmv摄像头所拍摄到的画面会产生畸变,通俗的说就是在画面边缘的图像会被拉长,这会对我们的坐标准度有一定的干扰,而openmv官方例程里提供了一个解决这个问题的代码,根据我们画面的具体实际情况来改变如下的参数可以一定程度的改善画面畸变的情况。

img = sensor.snapshot().lens_corr(strength = 1.4, zoom = 1.0)

LCD屏幕的使用:

在比赛时因为无法使用电脑,所以要知道摄像头看到了什么就得装上LCD屏幕来充当眼睛

import sensor
import display

sensor.reset()  # Initialize the camera sensor.
sensor.set_pixformat(sensor.RGB565)  # or sensor.GRAYSCALE
sensor.set_framesize(sensor.QQVGA2)  # Special 128x160 framesize for LCD Shield.
lcd = display.SPIDisplay()  # Initialize the lcd screen.

while True:
    lcd.write(sensor.snapshot())  # Take a picture and display the image.

但是在实际应用过程中,当函数img = sensor.snapshot().lens_corr(strength = 1.4, zoom = 1.0)的使用与LCD屏幕的使用同时出现时,屏幕画面会有诡异的跳动,这个问题作者也是一直没有解决,这个是我的唯一不足之处。

四、总结

因为自己硬件能力的不足,我为了这份代码花进去了很多心血,写出了很多种对干扰进行滤波的函数,我相信我的这次经验以及代码分享可以帮助到更多想要学习的人。

博主发布这篇文章时也只是刚读完大一,能力尚有不足,第一次发文表达能力也欠佳,也希望各位读者们多多包容。大家有什么不懂的或者我阐述得不清楚的地方也可以发在评论区,我也会尽力去帮助每一个人!

  • 29
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值