我的知名围棋APP忘忧围棋的开发者(www.gog361.com),一直想做一个可以通过手机拍摄识别死活题的功能,前后经过了半年时间的折腾,终于上线这个功能。这个过程很艰辛,并且踩了还不少的坑,所以记录下这个过程。

应用的场景有以下几种

1. 小孩子在学围棋的时候,家长是不懂围棋的,老师给的题目在书本上,家长可以拍照识别题目并通过AI解题

2. 在现实中下棋的时候,棋局结束的时候数子,或者棋局中间的时候形势判断,都可以拿起手机拍照后数子后进行形势判断,AI推荐,AI分析,AI复盘等

3. 即使在线上下棋也可以拍照识别后进行形势判断,AI推荐,AI分析,AI复盘。

第一阶段,试图通过传统OpenCV来识别围棋盘,最终失败

由于围棋是比较规则的,所以第一阶段试图通过传统的OpenCV识别完成,这个过程采用了OpenCV识别直线,识别圆形,识别角点等各种方法,都无法获得一个很好的效果。这个过程也发现对于完整棋局来说,找到四个棋盘四个角的位置至关重要。

1. 直线识别失败的原因是,棋盘外面会出现直线,导致很难区分那条线是棋盘的线,那条线是棋盘上的线,并且特殊情况,棋子完整的占满了棋盘后,也没有直线可以识别。

2. 识别圆形失败的原因是,不同的白棋子很难识别出来

3. 通过harris角点取得了一些效果,可以找到棋盘上的一些关键点,但是能解决一部分棋盘,但是有些棋盘根本无法解决。

4. findContours方法获得结果,有时候会把棋子分割成出来

5. 综合以上的方法合并起来,虽然能解决一些CASE但是几乎没有什么通用性。

第一阶段主要用的棋盘案例

第二阶段,转向深度学习来解决问题

经过第一阶段摸索后,确认了传统的识别方法是无法实现想要的效果的,决定转向深度学习方法。刚开始使用caffe SSD没有取的很好的效果,后来在公司AI大牛的建议下换到了YOLOV3,这里要特别感谢我的搭档易博士在训练方面的大力帮助,并且亲自使用自己的机器帮忙训练。下面记录下实现这个产品功能的详细过程。

1. 训练数据的准备

由于围棋棋盘上的棋子很多,一个一个手工标注是一个痛苦的过程,所以采用手工加程序帮忙的方式。

训练目标是要识别出,黑子,白子,角(无棋子角),边线(无棋子的边线)和空位置。如下图所示:

对于全局棋盘,首先只记录四个角的坐标,然后通过传统的方法计算出所有的元素并生成训练的VOC数据,这里主要用python实现

全局棋盘的角坐标用一个文本文件表示内容如下:

#顺序是lefttop,righttop,leftbottom,rightbottom, not solid whitestone 
00001,50,364,1034,364,50,1348,1034,1348,0
00002,50,364,1034,364,50,1348,1034,1348,0
00003,50,364,1034,364,50,1348,1034,1348,0
00004,66,472,1014,472,66,1420,1014,1420,0
00005,66,472,1014,472,66,1420,1014,1420,0
00006,36,410,1044,410,36,1420,1044,1420,0
00007,36,410,1044,410,36,1420,1044,1420,0

在棋盘倾斜的情况下,如何用四个角的坐标计算整个棋盘19X19的位置坐标,这里用到OpenCV的透视变换,代码如下:

def PointPerspectiveTransform(self, src, h):    
        u=src[0];
        v=src[1];
        x=(h[0][0]*u+h[0][1]*v+h[0][2])/(h[2][0]*u+h[2][1]*v+h[2][2]);
        y=(h[1][0]*u+h[1][1]*v+h[1][2])/(h[2][0]*u+h[2][1]*v+h[2][2]);
        return [x,y]
    def getBoardGird(self):
        src = np.array([[0,0],[1800,0],[0,1800],[1800,1800]],np.float32)
        dst = np.array([self.lefttop,self.righttop,self.leftbottom,self.rightbottom],np.float32)
        h = cv2.getPerspectiveTransform(src,dst)
        for x in range(0, WY_BOARD_LINES):
            for y in range(0, WY_BOARD_LINES):
                src=[x*100,y*100]
                des=self.PointPerspectiveTransform(src,h)
                #print(des)
                self.boardgrid[x][y]=des

函数getBoardGird就是通过一个规则的棋盘透视变换到四个角然后再反过来计算出棋盘所有19X19位置的坐标。

然后根据坐标的位置判断出是黑子还是白子。最后输出为VOC格式的XML

这里还有一个关键点是,为了增加更多的训练样本,根据这些图片本身的变换角度,应用在其他的图片上,自动生成更多角度的图片,代码如下:

def CreateMorePerspective(self):
        self.org_lefttop=self.lefttop
        self.org_righttop=self.righttop
        self.org_leftbottom=self.leftbottom
        self.org_rightbottom=self.rightbottom
        for x in range(0, WY_BOARD_LINES):
            for y in range(0, WY_BOARD_LINES):
                self.org_boardgrid[x][y]=self.boardgrid[x][y]
        plist=self.corner_datas_need_p['plist']
        count=len(plist)
        print("corner_datas_need_p len is:", count)
        for i in range(0,count):
            #temp=random.random()*10000
            #print("rang temp=,",temp,int(temp%10))
            #if int(temp%10)<=1:  
            corners=plist[i]
            lefttop=[int(corners[0]),int(corners[1])]
            righttop=[int(corners[2]),int(corners[3])]
            leftbottom=[int(corners[4]),int(corners[5])]
            rightbottom=[int(corners[6]),int(corners[7])]
            self.CreateNewImage(i,lefttop,righttop,leftbottom,rightbottom)

corner_datas_need_p 记录了所有不是正方形的棋盘的数据,然后任何一张图片都按照这个比例进行透视变换以生成更多的训练图片。

def CreateNewImage(self,index,lefttop,righttop,leftbottom,rightbottom):
        self.lefttop=lefttop;
        self.righttop=righttop
        self.leftbottom=leftbottom
        self.rightbottom=rightbottom
        src = np.array([self.org_lefttop,self.org_righttop,self.org_leftbottom,self.org_rightbottom],np.float32)
        dst = np.array([lefttop,righttop,leftbottom,rightbottom],np.float32)
        h_perspective = cv2.getPerspectiveTransform(src,dst)
        temp=GetDistance(self.lefttop,self.rightbottom)*1.2
        temp2=GetDistance(self.org_lefttop,self.org_rightbottom)*1.2
        if temp/temp2<0.5: #太小的就不转换了
            return
        if temp<temp2:
            temp=temp2
        self.img_perspective = cv2.warpPerspective(self.org_bgr, h_perspective, (int(temp), int(temp)))
        self.bgr=self.img_perspective.copy()
        for x in range(0, WY_BOARD_LINES):
            for y in range(0, WY_BOARD_LINES):
                src=self.org_boardgrid[x][y]
                des=self.PointPerspectiveTransform(src,h_perspective)
                #print(des)
                self.boardgrid[x][y]=des
        self.OutputToXML_CreateNewImg(index)

#在生成一个左上角1/4棋盘的图片
        they=int(self.boardgrid[9][9][1])
        thex=int(self.boardgrid[9][9][0])
        self.bgr=self.bgr[0:they, 0:thex]
        self.OutputToXML_CreateNewImg_Quarter(index)

以上是生成全局棋盘的训练数据。

对于死活题目的数据,这个要复杂很多,所以需要手工录入更多的数据以来定位,死活题目就不需要再识别黑白子了,直接采用录用的方法,原始数据的格式如下:

这里记录下四个顶点位置的围棋坐标和像素坐标,以及黑白子的围棋坐标,仍然通过openCV的透视变化去定位棋盘上的坐标。

{
    "imglist":
    [
    {
        "imgno" : 10000,
        "lefttop": [0,11,42,33],
        "righttop": [8,11,539,32],
        "rightbottom": [8,18,539,468],
        "leftbottom": [0,18,42,468],
        "blackstones":[0,16,1,16,2,16,3,16,4,17,3,18],
        "whitestones":[0,15,1,15,2,15,3,15,4,16,5,16,5,17,5,18],
        "addposition": []
    },
    {
        "imgno" : 10001,
        "lefttop": [0,12,124,111],
        "righttop": [5,12,961,108],
        "rightbottom": [5,18,874,918],
        "leftbottom": [0,18,222,989],
        "blackstones": [0,16,1,16,2,16,3,16,4,17,3,18],
        "whitestones": [0,15,1,15,2,15,3,15,4,16,5,16,5,17,5,18],
        "addposition": [6,18]
    },
    {
        "imgno" : 10002,
        "lefttop": [0,12,229,71],
        "righttop": [6,12,1073,292],
        "rightbottom": [6,18,795,1014],
        "leftbottom": [0,18,173,746],
        "blackstones": [0,16,1,16,2,16,3,16,4,17,3,18],
        "whitestones": [0,15,1,15,2,15,3,15,4,16,5,16,5,17,5,18],
        "addposition": [7,18,7,17,7,16]
    }。

对于死活题目,也全局棋盘一样的,通过透视变换生成更多的训练数据。

def CreateMorePerspective(self):
        self.org_lefttop=self.lefttop_pos
        self.org_righttop=self.righttop_pos
        self.org_leftbottom=self.leftbottom_pos
        self.org_rightbottom=self.rightbottom_pos
        for x in range(0, WY_BOARD_LINES):
            for y in range(0, WY_BOARD_LINES):
                self.org_boardgrid[x][y]=self.boardgrid[x][y]
        plist=self.corner_datas_need_p['plist']
        count=len(plist)
        print("corner_datas_need_p len is:", count)
        for i in range(0,count):
            corners=plist[i]
            #temp=random.random()*10000
            #print("rang temp=,",temp,int(temp%10))
            #if int(temp%10)<=1:
            lefttop=[int(corners[0]),int(corners[1])]
            righttop=[int(corners[2]),int(corners[3])]
            leftbottom=[int(corners[4]),int(corners[5])]
            rightbottom=[int(corners[6]),int(corners[7])]
            self.CreateNewImage(i,lefttop,righttop,leftbottom,rightbottom)
    
    def CreateNewImage(self,index,lefttop,righttop,leftbottom,rightbottom):
        self.lefttop=lefttop;
        self.righttop=righttop
        self.leftbottom=leftbottom
        self.rightbottom=rightbottom
        src = np.array([self.org_lefttop,self.org_righttop,self.org_leftbottom,self.org_rightbottom],np.float32)
        dst = np.array([lefttop,righttop,leftbottom,rightbottom],np.float32)
        h_perspective = cv2.getPerspectiveTransform(src,dst)
        temp=GetDistance(self.lefttop,self.rightbottom)*1.2
        temp2=GetDistance(self.org_lefttop,self.org_rightbottom)*1.2
        if temp/temp2<0.5: #太小的就不转换了
            return
        if temp<temp2:
            temp=temp2
        self.img_perspective = cv2.warpPerspective(self.org_bgr, h_perspective, (int(temp), int(temp)))
        self.bgr=self.img_perspective.copy()
        for x in range(0, WY_BOARD_LINES):
            for y in range(0, WY_BOARD_LINES):
                src=self.org_boardgrid[x][y]
                des=self.PointPerspectiveTransform(src,h_perspective)
                #print(des)
                self.boardgrid[x][y]=des
        self.OutputToXML_CreateNewImg(index)

2. 数据准备好后就开始训练

接下来就是训练的过程了,把所有全局棋盘的数据和死活题目的数据整合起来生成VOC数据集合。

训练的关键点在cfg文件里设置

angle=45

classes=5

filters=30

max=400

详情可参考yolov3的训练相关文档

训练的结果如下图

整体的map达到99.3%,黑子99.6%,白子99.56%,空99.6%,边线99.4%,角98.45%。

3. 训练好的网络的应用

由于要持续改进识别效果所以识别是放在服务器端进行的,用户上传照片到服务器端,服务器识别后把结果返回。所以服务采用c++实现。

训练好的网络只是返回了各个对象的位置,并且还存在可能性会丢失一些对象,所以服务器是采用网络和OpenCV混合的模式进行的,下面简要的写一下大致的过程:

首先要加载网络:

void CBoardDetect::LoadNet()
{
    clock_t start, finish;
    start = clock();
    string modelConfiguration = "/opt/goimage/model/all_v4.cfg";
    string modelWeights = "/opt/goimage/model/all_v4.weights";
    // Load names of classes 读取分类类名
    string classesFile = "/opt/goimage/model/all.names";
    ifstream ifs_all(classesFile.c_str());
    string line;
    while (getline(ifs_all, line))
    {
        m_classes_all.push_back(line);
    }

// Load the network 导入网络
    CBoardDetect::m_net_all = readNetFromDarknet(modelConfiguration, modelWeights);

finish = clock();
    cout << "LoadNet time is " << double(finish - start) / CLOCKS_PER_SEC << endl;
    //
}

然后通过网络前向读取识别的结果

int CBoardDetect::All_Net_Forward(Mat& src)
{
    m_width=m_bgr.cols;
    m_height=m_bgr.rows;
    m_smalledge=m_width>m_height?m_height:m_width;
    int a=time(NULL);
    clock_t start, finish;
    start = clock();
    Mat inputBlob = dnn::blobFromImage(src, 1.0/255, Size(416,416), NULL, true, false);//这里因为训练时对图像做了归一化,所以在推理的时候也要对图像进行归一化
    CBoardDetect::m_net_all.setInput(inputBlob);
    vector<Mat> outs;
    std::vector<String> outNames = CBoardDetect::m_net_all.getUnconnectedOutLayersNames();
    CBoardDetect::m_net_all.forward(outs, outNames);
    PostProcess_All(src, outs, CBoardDetect::m_net_all, mythresh, mynms); 
    finish = clock();
    int b=time(NULL);
    cout << "All_Net_Forward time is " << double(finish - start) / CLOCKS_PER_SEC << endl;
    
    return 0;  
}

void CBoardDetect::PostProcess_All(Mat& frame, const std::vector<Mat>& outs, Net& net, float mythresh, float mynms)
{
    static std::vector<int> outLayers = net.getUnconnectedOutLayers();
    static std::string outLayerType = net.getLayer(outLayers[0])->type;
    std::vector<int> classIds;
    std::vector<float> confidences;
    std::vector<Rect> boxes;
    if (outLayerType == "Region")
    {
        for (size_t i = 0; i < outs.size(); ++i)
        {
            //网络输出的数据是一个NxC矩阵向量,N是检测到的目标数量,C的类别数 + 4
            //开始的4个数据是[center_x, center_y, width, height]
            float* data = (float*)outs[i].data;
            for (int j = 0; j < outs[i].rows; ++j, data += outs[i].cols)
            {
                Mat scores = outs[i].row(j).colRange(5, outs[i].cols);
                Point classIdPoint;
                double confidence;
                minMaxLoc(scores, 0, &confidence, 0, &classIdPoint);
                if (confidence > mythresh)
                {
                   /* if(classIdPoint.x==1) // this is board
                    {
                        m_vectBorder_Points.push_back(cv::Point2f(data[0] * frame.cols,data[1] * frame.rows));
                        if(m_object_min_width>data[2] * frame.cols)
                        {
                            m_object_min_width=data[2] * frame.cols;
                        }
                    }*/

int centerX = (int)(data[0] * frame.cols);
                    int centerY = (int)(data[1] * frame.rows);
                    int width = (int)(data[2] * frame.cols);
                    int height = (int)(data[3] * frame.rows);
                    int left = centerX - width / 2;
                    int top = centerY - height / 2;

classIds.push_back(classIdPoint.x);
                    confidences.push_back((float)confidence);
                    boxes.push_back(Rect(left, top, width, height));
                }
            }
        }
        std::vector<int> indices;
        NMSBoxes(boxes, confidences, mythresh, mynms, indices);
        m_vectAllBoxs.clear();
        for (size_t i = 0; i < indices.size(); ++i)
        {
            int idx = indices[i];
            Rect box = boxes[idx];
            NNBoxInfo boxinfo;
            boxinfo.id=m_vectAllBoxs.size();
            boxinfo.classid=classIds[idx];
            float confidence=confidences[idx];
            boxinfo.confidence=confidence;
            if(boxinfo.classid>1) //box 1.5 times larger
            {
                int centerX = box.x+box.width/2;
                int centerY = box.y+box.height/2;
                int width=box.width*3/2;
                int height=box.height*3/2;
                box=Rect(centerX-width/2,centerY-height/2,width,height);
                boxinfo.box=box;
            }
            else
            {
                boxinfo.box=box;
            }
            m_vectAllBoxs.push_back(boxinfo);
           /* DrawPrediction(CBoardDetect::m_classes_all, classIds[idx], confidences[idx], box.x, box.y,
                 box.x + box.width, box.y + box.height, frame);*/
        }
    }
   /* string strtemp=m_strFilePath+"-yolo_all.jpg";
    strtemp=replace(strtemp,"imgtest","testoutput");
    printf("output file %s\n",strtemp.c_str());
    imwrite(strtemp.c_str(),frame);*/
}

最后把数据存入一个NNBoxInfo的数据结构,这个数据结构会建立一个位置的关系,NNBoxInfo的定义如下

class NNBoxInfo
{
public:
    NNBoxInfo()
    {
        fDist_Base=0.0;
        classid=-1;
        neighbor_count=0;
        left=NULL;
        lefttop=NULL;
        top=NULL;
        righttop=NULL;
        right=NULL;
        rightbottom=NULL;
        bottom=NULL;
        leftbottom=NULL;
        x=INVALID_COORD;
        y=INVALID_COORD;
    }
    ~NNBoxInfo()
    {

}
    int  id;
    Rect box;
    int classid;
    float confidence;
    int neighbor_count;
    NNBoxInfo * left;
    NNBoxInfo * lefttop;
    NNBoxInfo * top;
    NNBoxInfo * righttop;
    NNBoxInfo * right;
    NNBoxInfo * rightbottom;
    NNBoxInfo * bottom;
    NNBoxInfo * leftbottom;
    float fDist_Base;
    int  x;
    int  y;
    
    NNBoxInfo * neighbor[MAX_NEIGHBOR];

然后通过一系列的运算后建立一个位置的关系,最后计算出棋盘各个位置棋子的颜色。通过还通过OpenCV的线条来校验计算的结果,并且通过OpenCV的EMD算法对比棋子,找到可能没有发现的棋子。

另外补充一点通过算法来区分是全局棋谱,死活题,9路还是13路围棋。

客户端的实现,小程序下的代码:

scanProcess(imgpath)
  {
    console.log("imgpath=",imgpath);
    wx.showLoading({
      title: '识别中...',
    })
    var that=this;
    let posturl="https://app.gog361.com/flask/v1/app/ai/scanimage/"+app.globalData.userid;
     wx.uploadFile({
      url: posturl,
      filePath: imgpath, 
      name: 'file',
      header: { "Content-Type": "multipart/form-data" },
      formData: {
        //和服务器约定的token, 一般也可以放在header中
        'userid': app.globalData.userid,
        'sessionkey': app.globalData.sessionkey,
        'session_token': wx.getStorageSync('session_token')
      },
      success: function (res) {
        console.log(res);
        if (res.statusCode != 200) { 
          wx.showModal({
            title: '提示',
            content: '上传失败',
            showCancel: false
          })
          return;
        }
        var data = JSON.parse(res.data);
        if(data.code>=0)
        {
            that.startresult(data);
        }
        else
        {
            wx.showModal({
              title: '提示',
              content: '可能因为光线原因识别失败,请稍微换个角度重拍',
              showCancel: false,
              success: function(res) 
              {
                that.start_rescan();
              }
            })
        }

return;
      },
      fail: function (e) {
        console.log(e);
        wx.showModal({
          title: '提示',
          content: '上传失败',
          showCancel: false
        })
      },
      complete: function () {
        wx.hideLoading();
      }
    })
  }

最后对比下原图和拍照识别后的结果

总结:目前对全局棋盘识别的效果比较好,对死活题识别的效果要差一些,需要收集更多的死活题目,进行新一轮的训练来改进效果。

想象空间,有机会做到使用普通的围棋盘可以做到智能棋盘的效果,在普通棋盘上面架一个手机通过识别后能够使用普通棋盘进行任何的线上对弈,人机对弈做死活题等功能, 保护视力。

最后有兴趣的朋友可以去微信搜索小程序"忘忧围棋题库”或者去苹果Appstore以及安卓市场下载“忘忧围棋”体验。

使用yolov3训练识别围棋死活题和围棋局面相关推荐

  1. 『论文笔记』TensorFlow1.6.0+Keras 2.1.5+Python3.5+Yolov3训练自己的数据集!

    TensorFlow1.6.0+Keras 2.1.5+Python3.5+Yolov3训练自己的数据集! 文章目录 前期准备 一. Yolov3简要介绍 1.1. Yolov3网络结构图 1.2. ...

  2. 光量子计算机 围棋,最强围棋AI,玩起了量子计算?

    AlphaZero是DeepMind围棋软件AlphaGo的升级版.虽然AlphaZero在围棋项目上战胜了人类选手,但所需的大量算力使其很难走进寻常人的生活.最近,丹麦和德国的研究人员使用Deepm ...

  3. YOLOv3 训练的各种config文件以及weights文件。

    YOLOv3训练过程中的各种文件.包括配置文件,权重文件. yolov3.pt yolov3.weights darknet53.conv.74 yolov3-spp.weights yolov3-t ...

  4. YOLOv3: 训练自己的数据(绝对经典版本1)

    为什么80%的码农都做不了架构师?>>>    windows版本:请参考:https://github.com/AlexeyAB/darknet linux       版本:请参 ...

  5. TF之TFOD-API:基于tensorflow框架利用TFOD-API脚本文件将YoloV3训练好的.ckpt模型文件转换为推理时采用的.pb文件

    TF之TFOD-API:基于tensorflow框架利用TFOD-API脚本文件将YoloV3训练好的.ckpt模型文件转换为推理时采用的frozen_inference_graph.pb文件 目录 ...

  6. 从零开始带你一步一步使用YOLOv3训练自己的数据

    红色石头的个人网站:redstonewill.com 知乎:https://www.zhihu.com/people/red_stone_wl 公众号:AI有道(redstonewill) YOLOv ...

  7. yolov3目标检测android,目标检测 | YOLOv3训练自己的数据全流程

    1.构建YOLOv3网络的cfg文件 该文件表示的是你的检测网络的结构,类似caffe的prototxt文件. YOLOv3的cfg文件 上篇介绍YOLOv3网络中提到的去掉上采样操作的YOLOv3c ...

  8. yolov3训练误差可视化

    通过把yolov3训练出来的效果可视化: 可以用python可视化,也可以直接在代码里面用tensorboardX可视化. import re import pandas as pd from mat ...

  9. WIN10 +pytorch版yolov3训练自己数据集

    pytorch版yolov3训练自己数据集 目录 1. 环境搭建 2. 数据集构建 3. 训练模型 4. 测试模型 5. 评估模型 6. 可视化 7. 高级进阶-网络结构更改 1. 环境搭建 将git ...

最新文章

  1. php 判断是否gzip,PHP网站判断页面文件或图片是否经过gzip压缩
  2. AttributeError: module ‘matplotlib’ has no attribute ‘artist’
  3. Python 统计一行字符中单词的个数_Python 经典练习题-015
  4. 怎样让防火墙跟其他网络设备实现时钟同步
  5. 手把手教你用java完成文件、图片下载
  6. 德国沃达丰成功商用4.5G 德国最快基站峰值速率达375Mbps
  7. 今天我的天空瞬间明亮了
  8. opencv 获取图像最大连通域 c++和python版
  9. WiFi Explorer Mac版WiFi管理器常见问题解答
  10. GB28181协议错误码返回码整理
  11. Django面试题汇总
  12. 关于linux系统安装zabbix报错的解决方案
  13. 2020年阴历二月二十八 投资理财~如何正确面对黑天鹅
  14. request域中放入参数几种方法
  15. 【原创】十年可以做什么?
  16. 科技金融企业助力乡村振兴,能有多大新意?
  17. OpenJudge NOI 1.8 20:反反复复
  18. Java switch和break用法
  19. debug疯了_《尼尔机械纪元》调试房间Debug模式开启及设置教程 Debug模式怎么进...
  20. CentOS之——双网卡双IP双网关配置(双网卡配置一个上外网一个接局域网)

热门文章

  1. 电路(二)电阻电路的等效分析(附二元泰勒公式)
  2. 爬虫日记之07正则表达式(手把手教你区分贪婪匹配和惰性匹配)
  3. [Hack The Box] HTB—Paper walkthrough
  4. 阿里云配置密钥SSH登录
  5. 浙江省杭州工程师职称申报方式
  6. Fisher线性判别 模式识别 例题
  7. QST《Linux基础》学习笔记
  8. PPT动画中点击、之前、之后的区别
  9. STM32 SPI NSS大揭秘
  10. 宜宾市中小学足球调研现状