December 6, 2021

디바이스마트 미디어:

[66호] 원하는 색상으로 제어가 가능한 아두이노 IoT 스마트 무드등 키트 -

2021-06-25

★2021 ICT 융합 프로젝트 공모전 결과 발표! -

2021-05-12

디바이스마트 국내 온라인 유통사 유일 벨로다인 라이다 공급! -

2021-02-16

★총 상금 500만원 /2021 ICT 융합 프로젝트 공모전★ -

2021-01-18

디바이스마트 온라인 매거진 전자책(PDF)이 무료! -

2020-09-29

[61호]음성으로 제어하는 간접등 만들기 -

2020-08-26

디바이스마트 자체제작 코딩키트 ‘코딩 도담도담’ 출시 -

2020-08-10

GGM AC모터 대량등록! -

2020-07-10

[60호]초소형 레이더 MDR, 어떻게 제어하고 활용하나 -

2020-06-30

[60호]NANO 33 IoT보드를 활용한 블루투스 수평계 만들기 -

2020-06-30

라즈베리파이3가 드디어 출시!!! (Now Raspberry Pi 3 is Coming!!) -

2016-02-29

MoonWalker Actuator 판매개시!! -

2015-08-27

디바이스마트 레이저가공, 밀링, 선반, 라우터 등 커스텀서비스 견적요청 방법 설명동영상 입니다. -

2015-06-09

디바이스마트와 인텔®이 함께하는 IoT 경진대회! -

2015-05-19

드디어 adafruit도 디바이스마트에서 쉽고 저렴하게 !! -

2015-03-25

[29호] Intel Edison Review -

2015-03-10

Pololu 공식 Distributor 디바이스마트, Pololu 상품 판매 개시!! -

2015-03-09

[칩센]블루투스 전 제품 10%가격할인!! -

2015-02-02

[Arduino]Uno(R3) 구입시 37종 센서키트 할인이벤트!! -

2015-02-02

[M.A.I]Ahram_ISP_V1.5 60개 한정수량 할인이벤트!! -

2015-02-02

[67호]딥러닝을 결합시킨 임베디드 기구 오목 AI : 베타오

67_ICT_대상_오목AI 베타오(1)

 

2021 ICT 융합 프로젝트 공모전 대상

딥러닝을 결합시킨 임베디드 기구 오목 AI : 베타오’

글 | 호서대학교 박정준, 김진우, 박정섭, 김선혁, 장서윤, 이성용, 이준용

1. 심사평
칩센 Vision을 활용한다고 했을 때 바둑판과 바둑돌은 매우 명확하게 구분이 가능한 환경이라 볼 수 있겠습니다. 따라서 작품의 완성도는 AI의 성능과 robot의 움직임을 포함한 성능이 될 것으로 보여졌습니다. 동영상 및 보고서상으로 확인한 바로는 프로파일을 LM 가이드로 움직임에 따라 상부에 설치된 카메라에 간섭이 자주 발생하게 되는 부분이 구조적인 아쉬운 느낌이 들었습니다. 향후 실제 경기용으로 적용을 하기 위해서는, 현재의 작품은 사람과 AI 데이터를 기반으로한 로봇간의 대결이나, 로봇간의 대결을 통한 머신/딥러닝 과정을 진행함으로써 쌓인 데이터 축적으로 난이도와 완성도를 높일 수 있을 듯합니다.

펌테크 딥러닝, AI로봇, 영상처리 등의 난이도가 있는 기술을 효율적으로 접목하여 기획의도에 맞게 시스템을 안정적이면서 완성도 높게 구현한 인상적인 작품으로 작품의 아이디어와 창의성이 돋보이며 전체적으로 기획의도, 기술 구현도, 완성도 등에서 상당히 뛰어나고 우수한 작품으로 생각됩니다.

위드로봇 영상처리, DNN 학습 및 응용, 로봇 제어가 모두 포함된 완성도 높은 작품입니다.

뉴티씨 요즘 핫이슈이고 AI를 배워서 나오게 되면 연봉도 많이 받을 수 있다고 하죠? 어딜가도 AI가 대세입니다. AI를 접목하고 CNC 등을 동작시켜 오목을 둘 수 있도록 한 아이디어가 참 좋습니다. 로봇도 학습하고 AI도 학습하고. 교육 목적으로도 참 좋은 작품 같습니다. 비슷한 기술로 생각해보면 카메라로 사진을 찍고, 그것을 영상처리하여 스케치한 것처럼 만들고, 그것을 그리는 로봇도 쉽게 만들 수 있을 것 같습니요. 수고많으셨습니다.

2. 작품 개요
제작 동기
로봇자동화공학과에 재학 중 ‘AI 인공지능과 AI 인공지능을 직접 눈으로 볼 수 있는 하드웨어를 구상하던 중 오목 AI와 오목을 둘 수 있는 하드웨어를 구상해보자’는 아이디어를 얻어 Omok AI 베타오를 제작하게 되었다.

제작 목적
알파고와 이세돌의 경기 후, 공학계에서는 인공지능의 중요도가 급부상했다. 하지만 교육적으로 임베디드에 AI를 활용한 사례는 극히 드물다. SW 교육과 AI 활용에 도움이 될 수 있는 AI 오목 게임 로봇이다.

작품 요약
Omok AI는 gomocup의 데이터를 이용하여 CNN 기반으로 최적의 다음 수를 둘 수 있는 AI이다. 이를 통해 openCV로 검은돌(사용자)가 돌을 둔 곳의 위치를 인식 받아 아두이노와 하드웨어가 3D 프린터, CNC 가공 기계와 같이 움직여 오목의 돌을 옮겨주는 로봇이다.

3. 작품 설명
작품 주요 기능
작품 주요 기능은 크게 하드웨어, 소프트웨어, 임베디드 시스템 3가지로 구성되어 있다.
하드웨어는 카메라가 고정되어 달리고 모터가 x, y, z 3차원 직교좌표계를 따라 움직이는 본체이다. 서보, 리니어, 스텝 모터 3종류를 제어해 3D 프린터와 비슷하게 일정 높이에서 돌을 이동시킨다. 이로써 플레이어가 손을 대지 않아도 컴퓨터와 게임에 참여할 수 있는 것을 확인할 수 있다.
소프트웨어는 크게 오목 경기를 화면 GUI상에서 확인하고 게임에 참여할 수 있도록 짜인 모듈의 집합체인 GUI 프로그램, 경기를 할 수 있는 Ai 학습 과정 전체에 대한 학습 코드, 파이썬과 아두이노를 통신, 아두이노를 제어하는 코드 세 가지로 나뉜다.
GUI 프로그램은 게임에 참여하는 플레이어가 오목 경기를 하는 도중 사용할 오목판 GUI의 색과 좌표, 돌의 크기 등을 속성값으로 변환할 수 있다. 또 오목의 룰을 정의해 게임의 승패 조건을 판단하고 정의한다. 이때 사용되는 오목의 룰은 Freestyle(자유룰)로 오목돌이 5개 이상 모였을 경우에 승리하는 룰을 사용한다.
학습 코드는 gomocup에서 2020년 결과 데이터를 얻어 CNN으로 학습한다. 오목 경기가 이루어진 수많은 데이터를 전처리해서, 컴퓨터-사람 간의 경기가 이루어질 수 있도록 게임의 룰을 이해하고 학습할 수 있도록 한다.
임베디드 시스템은 영상처리 과정을 거친 좌표값을 받아 하드웨어를 구성할 수 있는 제어 보드 및 프로세스이다. 모터제어를 위해 아두이노 우노를 사용해 시리얼 통신으로 PC로 좌표값을 보낸다. 메인 제어기인 아두이노 우노는 총 2개를 사용하며 각각 x축 스텝모터, y축 스텝모터와 리니어, 서보모터를 제어한다.

작품 활용 방안
카메라를 활용해 바둑판을 인식하고 원하는 좌표값을 받아 알맞은 위치에 돌을 두고, 오목 경기를 진행할 수 있도록 하는 작품이다. 실제 오목 경기를 연습할 수 있는 AI 심판을 제작하는 셈이지만, 임베디드에 AI를 적용시키는 프로젝트로써 관련 컨텐츠가 교재 및 강의와 함께 교육용 작품으로 활용할 수 있다.

3.1. 주요 동작 및 특징

67_ICT_대상_오목AI 베타오(2)

67_ICT_대상_오목AI 베타오(3)

3.2. 전체 시스템 구성
전체 시스템 구성도

67_ICT_대상_오목AI 베타오(4)
시스템은 다음과 같이 구성되어있다. 하드웨어와 임베디드 시스템은 PC를 통해 영상 처리 결과 정보 및 좌표값을 시리얼 통신을 통해 수신받는다. 웹캠-PC간에 영상처리 정보 결과가 양방향으로 전달되며, 파이썬 IDE 파이참이 시리얼 통신을 통해 아두이노에 좌표값을 전달한다. 사람과 AI가 플레이를 하는 동안 파이썬은 지속해서 게임의 룰을 지키며 다음 돌의 최적의 위치를 예측하여 두게 된다. 최종적으로 위의 세 구성 요소가 상호작용함에 따라 시스템이 동작한다.

세부 구성도

67_ICT_대상_오목AI 베타오(5)
세부 구성도는 [그림 4]와 같이 크게 HW, Embedded 시스템, PC/CAM, SW로 구성되어 있다.

하드웨어 구성도

67_ICT_대상_오목AI 베타오(6)
Embedded system 회로도

67_ICT_대상_오목AI 베타오(7)

67_ICT_대상_오목AI 베타오(8)

67_ICT_대상_오목AI 베타오(9)

소프트웨어

67_ICT_대상_오목AI 베타오(10)

3.3. 개발 환경
소프트웨어
파이썬 IDE 파이참, 학습을 위한 jupyter notebook

67_ICT_대상_오목AI 베타오(12)

모터 제어를 위한 Arduino IDE

67_ICT_대상_오목AI 베타오(13)

개발 환경

67_ICT_대상_오목AI 베타오(14)

Python 3.7.10 환경과 jupyter 가상환경 구축 및 실행

67_ICT_대상_오목AI 베타오(15)
Tensorflow 2.4.0 version & GPU RTX 2060 SUPER

67_ICT_대상_오목AI 베타오(16)

영상 인식(openCV)

67_ICT_대상_오목AI 베타오(17)

아두이노와 파이참 통신

67_ICT_대상_오목AI 베타오(18)

임베디드 시스템
임베디드 시스템 제작 시 준비물

67_ICT_대상_오목AI 베타오(19)

67_ICT_대상_오목AI 베타오(20)

하드웨어
사용 시스템 및 Tool
솔리드웍스, DP103, 3DWOX, 레이저 커터, 3D 펜 등을 가지고 작품을 제작하였으며 각각의 세부 설명은 다음과 같다. 솔리드웍스는 3D CAD 설계 Tool로 스텝 모터 고정 대, LM 가이드라인, 바둑돌 사출기, 스위치 고정 등에 사용되었으며 설계 파일 및 도면은 최종 원고에 사용한 자료에 포함되어 있다. 솔리드웍스의 버전은 Solidworks 2019 버전을 사용하였다.
3D 프린터는 Sindoh 사의 DP103을 사용하여 프린팅하였으며 stl 파일을 Gcode로 변환해주는 Tool은 3DWOX를 사용하였다. 또한 3D 펜을 사용하여 3D 프린터 출력물을 수정 보완하였으며 스위치 고정 지지대 출력 및 프로파일 고정대 출력 등에 사용하였다.

67_ICT_대상_오목AI 베타오(21)

HW 제작 시 준비물

67_ICT_대상_오목AI 베타오(22)

67_ICT_대상_오목AI 베타오(23)

 

67_ICT_대상_오목AI 베타오(24)

67_ICT_대상_오목AI 베타오(1)

4. 단계별 제작 과정
하드웨어(컨셉 설계도 및 설계도)

67_ICT_대상_오목AI 베타오(2)
초기 컨셉 설계도로 x, y축의 초기 설정과 베타오의 기본 베이스가 되는 설계도이다.

67_ICT_대상_오목AI 베타오(25)

베타오의 최종 완성된 사진이다. 최종 완성되기까지의 과정은 아래와 같은 과정을 거처 진행된다.

67_ICT_대상_오목AI 베타오(3)

사진과 같이 오목판의 크기를 측정하여 측정된 크기에 맞게 프로파일을 가공하여 연결한다. 모든 단계의 기초이자 가장 중요한 부분이므로 프로파일과 프로파일을 연결하는 알루미늄 다이캐스트 브라켓과 렌치볼트를 사용하여 단단하게 고정해야 한다.

67_ICT_대상_오목AI 베타오(26)

y축이 x축 위에서 움직일 수 있도록 해주는 LM 가이드 라인을 프로파일과 연결할 수 있도록 설계한 것이다. LM 가이드의 수평과 고정을 동시에 고려하며 설계한 설계 부품이다. 스테인레스 재질의 LM 가이드 라인DMF 사용하려 계획했었지만, 프로파일과의 정교한 고정과 비용적인 측면에서 어려움을 겪어 기존에 있는 스테인레스 재질의 LM 가이드 라인을 역설계하여 베타오에 맞게끔 설계하였다.

 

67_ICT_대상_오목AI 베타오(4)

[그림 47]와 같이 프로파일 지지대의 상층부를 프로파일과 알루미늄 다이캐스트 브라켓으로 연결한 후 수평을 맞추어준다. 이 과정에서 수평은 수평계를 활용하여 맞춘다. LM 가이드 라인과 LM 가이드가 최소한의 마찰을 가지며 움직여야 정확한 좌표를 얻을 수 있기 때문에 이 과정에 시간을 많이 투자해야 된다. 이 과정을 거치고 z축에 바둑돌 사출기를 장착하면 [그림 48]와 같아진다.

 

67_ICT_대상_오목AI 베타오(27)

스위치는 스텝 모터의 x, y축의 양단에 한계점의 신호를 받기 위해 2개를 장착한다. 각각의 스위치를 고정하는데 3D 펜을 사용하여 보다 정밀하게 제작하였다. 스위치의 위치가 바로 원점의 위치가 되므로 정밀하게 제작되어야 한다. [그림 50]와 같이 카메라를 고정하였는데 이는 openCV에서 바둑판의 전체를 인식 받아야 하기 때문에 중앙에 설치하게 되었다.

67_ICT_대상_오목AI 베타오(28)

[그림 51]와 [그림 52]의 ‘.SLDPRT파일’은 솔리드웍스를 활용한 y축, z축의 설계 부품이며 stl은 3D프린팅을 위한 Gcode 변환하기 전의 파일로 3DWOX Tool을 활용하여 Gcode로 변환하여 DP103 프린터를 이용하여 프린팅이 가능하다.

임베디드 시스템
아두이노 통신

67_ICT_대상_오목AI 베타오(29)

[그림 53]과 [그림 54]은 각각 파이썬에서 데이터를 정제해서 시리얼 통신으로 전달받은 데이터를 가지고 스텝 모터, 리니어 모터, 서보 모터를 움직이는 제어 코드, 좌표 데이터 정제 코드이다. 정확한 값을 받기 위해 조건을 여러번 설정하여 원하는 데이터가 아닌 노이즈, 채터링이 들어왔을 경우를 방지한다. 예를 들어 (3, 4)에 해당하는 좌표의 경우 각 좌표앞에 0을 추가 시켜 (03, 04)로 시리얼 통신한다.

67_ICT_대상_오목AI 베타오(30)

[그림 55]와 [그림 56]는 각각 파이썬에서 아두이노로의 좌표값 전달과 그 위를 나타낸 것이다.

67_ICT_대상_오목AI 베타오(31)

[그림 57]과 같이 아두이노 우노 2개를 파이썬과 시리얼 통신하여 x축, y축의 좌표값을 받아 각각 x축의 값이 정확할 때 LED를 켜봄으로써 정확한 좌표값이 들어갔다는 것을 확인할 수 있었으며 x축, y축의 값이 정확하게 들어간 것을 [그림 58]를 통해 확인할 수 있었다. 이를 통해 파이썬과 아두이노가 정확한 데이터 통신이 가능하다는 것을 볼 수 있다.

67_ICT_대상_오목AI 베타오(32)

[그림 59]와 같이 1개의 스위치와 1개의 스텝 모터를 제어함으로써 하나의 모터와 스위치가 정확한 값에서 움직이고 멈춘다는 것을 알 수 있다. 또한 [그림 60]와 x, y축에 해당하는 스텝 모터를 제어할 수 있다. 이 과정을 통해 베타오의 초기 셋팅이 가능함을 알 수 있다.

67_ICT_대상_오목AI 베타오(33)

리니어 모터와 서보 모터를 활용하여 z축의 바둑돌 사출기를 만들기 전 테스트를 위해 두 개의 모터만 따로 제어한 것이 [그림 61]이다. 리니어 모터를 통해 z축의 높이를 조절하며 서보 모터를 통해 바둑돌을 하나씩 바둑판으로 사출시킨다.

소프트웨어
알고리즘 및 코드 진행 과정

67_ICT_대상_오목AI 베타오(34)

알고리즘 및 코드 진행 과정을 간략화한 순서도이다. 각 과정을 통해 학습된 결과와 그 학습된 결과를 활용한 오목 GUI와 openCV 영상처리를 통한 좌표값이 서로 상호작용하며 아두이노 제어 보드에게 좌표값을 정제하여 전달한다.

파이참 환경 및 외장 함수 import

67_ICT_대상_오목AI 베타오(35)

numpy, glob, tqdm, os, ursina, tensorflow, pythonopenCV, serial 파이썬 외장 함수를 다운로드한다. 다운로드된 외장 함수를 이용하여 함수를 불러와 필요함수를 사용한다.

데이터 전처리 과정(데이터셋 생성 과정)

67_ICT_대상_오목AI 베타오(36)
create_dataset.py를 실행하여 gomocup2020results 폴더에 저장 되어 있는 ‘.psq’ 파일의 데이터를 바둑판의 크기와 돌의 위치를 받아온다. 전처리하기 전 데이터를 위와 같은 ‘.psg’ 파일로 저장되어 있는데 Fastgame, Freestyle, Renju, Standard와 같이 게임에 적용되는 룰이 다양하다. 그 중 Freestyle과 같은 게임 룰을 가지고 데이터셋을 만든다. ‘.psq’ 파일 중 학습에 필요한 데이터만을 ‘.npz’파일로 저장한다.

67_ICT_대상_오목AI 베타오(38)

[그림 67]와 [그림 68]은 각각 데이터를 전처리하는 코드로 gomocup에서 다운받은 2020년 데이터 ‘.psq’파일을 [그림 68]과 같이 ‘.npz’파일로 저장하는 코드이다.

67_ICT_대상_오목AI 베타오(37)

[그림 69]과 [그림 70]는 jupyter notebook을 통해 프로그램되는 코드이다. 파이썬 IDE 파이참에서 전처리된 데이터를 받아와서 CNN 기반의 모델을 만든다.

67_ICT_대상_오목AI 베타오(39)

[그림 71] 전처리된 데이터셋을 jupyter 환경에 저장하는 코드이고 [그림 72]은 학습에 사용할 신경망을 구축하는 코드이다. 또한 [그림 73]은 신경망에 대해 학습을 10번 진행한 결과로 정확도가 학습을 진행할수록 높아지는 것을 확인할 수 있다. 1번의 학습에 걸리는 시간은 약 1,900초이며, 학습 데이터의 양이 많아 32GB의 메모리가 필요하다. 10번의 학습이 끝나면 학습 결과를 [그림 74]과 같이 ‘.h5’로 저장한다.

67_ICT_대상_오목AI 베타오(40)

openCV(영상 인식)
먼저 바둑알 사진을 불러온다. 불러온 사진을 이진화하여 노이즈를 제거한다. 다음으로 실시간 영상을 불러온다. 실시간 영상 또한 이진화 처리하여 노이즈를 최대한 줄인다. 그 후 검은색을 인식하기 위해 픽셀의 값이 임계값(threshold) 이하이면 하얀색으로, 일정값 이상이면 검은색으로 픽셀의 값을 바꾼다. 이 과정을 통해 검은 바둑돌만 검정색으로, 다른 배경은 흰색으로 칠한다. 그 이후 처음 불러왔던 바둑돌과 비교하여 임계값(threshold) 이상의 정확도를 가질 경우 그 좌표를 불러온 사진의 크기만큼 초록색 사각형(컨투어)을 칠한다. 또한 사각형의 중심에 파란색 점을 찍는다. 그 점 값을 전처리를 통해 기존 검은돌의 좌표를 저장한 배열에 존재하지 않을면 값을 추가한다. 또한 새롭게 추가된 좌표값만 출력한다.
[그림 75]과 같이 검은돌을 바둑판에서 인식받아 정해진 임계값에 따라 컨투어를 생성하고 좌표를 생성한다.

좌표값을 인식해서 gui상에서 돌을 둠

67_ICT_대상_오목AI 베타오(41)

67_ICT_대상_오목AI 베타오(42)

5. 작품 결과

67_ICT_대상_오목AI 베타오(43)

6. 피드백 및 발전 방향
6.1. 피드백
베타오의 특징점은 PC가 사람과 직접 오목 게임을 플레이까지 할 수 있다는 점이다. 하지만 실제 경기에 활용할 수 있도록 하기 위해서 다음과 같은 부분이 해결되어야 한다.

난이도 조절이 필요하다.
학습 결과가 높을수록 컴퓨터가 이길 확률이 높기 때문에 정확도를 조절할 수 있는 학습 모델을 따로 생성해야 한다. .h5파일을 활용해 좀 더 낮은 수준의 인공지능을 적용할 수 있도록 수정할 예정이며, 경기에 적용했을 때의 난이도까지 테스트할 것이다.

33 규칙을 적용해야 한다.
실제 오목 경기에서는 한 번에 두 줄이 3, 3이 되는 경우는 이겼다고 규정하지 않는다. 승패가 무효가 되는 중요한 규칙이므로 이 점에 대해 보완할 것이다.

흑돌이 유리하다
오목은 확률론적으로 흑돌이 무조건적 우위를 선점한다. 현재 흑돌은 사람을 기준으로 선언되어있기 때문에 유저의 기호에 맞춰 턴을 바꿀 수 있도록 사전 UI를 구성하여 선택할 수 있도록 수정할 것이다.

6.2. 발전방향
1) 일반인이 직접 활용할 수 있는 딥러닝 교육용 제품으로 사용할 수 있다. 영상처리, 시리얼 통신, 임베디드 시스템, 딥러닝을 결합한 교육용 SW로써 각 분야를 하나의 시스템으로 구성하는 경험을 할 수 있다.
2) 자율주행 및 자동화 공정과 같은 실제 시스템에 응용해서 사용할 수 있다. 영상처리 기반으로 좌표 값을 추출하여 스텝 모터를 기반으로 정밀하게 좌표값 위치로 이동시킨다. 이는 정밀공정과 같은 다양한 시스템에 적용이 가능하다.
3) 오목 경기 연습 AI 및 학습 연구 대상이 될 수 있다. 또한 현재 개발된 모델과 매우 흡사한 바둑과 체스 등 다양한 시스템에 적용할 수 있다. 알파고의 출범 이후, 프로 바둑 기사들은 인공지능의 수와 흡사하게 두는 방법으로 실력을 키우고 있다. 이와 비슷한 맥락으로 본 프로젝트로 개발된 오목 AI도 오목 프로 기사를 양성하는 과정에서 사용될 수 있다.
4) 이벤트성 행사 및 공공일정에서 사용할 수 있다. 베타오는 소프트웨어가 하나의 압축 디렉토리로 송수신이 가능하기 때문에 하드웨어만 준비될 수 있다면 충분히 전시회 및 박람회에서 사용할 수 있는 행사 기구로 발전할 수 있다.

기술적 측면에서의 발전 방향
본 프로젝트는 기술적으로 크게 MUC 기반의 임베디드 제어 시스템, 바둑알을 인식하기 위한 영상처리, 오목 알고리즘 구현을 위한 CNN 기반 AI 등의 SW 부분과 리드 스크류, LM가이드를 이용한 오목 로봇의 좌표 이동, 리니어 모터를 이용한 바둑알 사출 시스템 등의 HW 부분으로 구분할 수 있으며 각 분야별 발전 방향은 다음과 같다.

소프트웨어
영상 인식
고사양 GPU를 사용해 처리 속도를 향상시킨다. 이는 영상 인식과 모델 생성 속도에도 영향을 주며 GPU의 성능에 따라 증가한다. 또한 사용하고 있는 openCV 정적라이브러리(static library)를 동적 라이브러리의 사용으로 변환하면 최소한의 프로그램 사용으로 같은 수준의 영상 인식을 사용할 수 있어 간단해진다.

오목 AI
생성된 모델을 모델로 학습하는 GAN(Generative Adversarial Network)을 사용한다. GAN을 사용하게 되면 생성 모델의 성능을 압도적으로 향상시킬 수 있다.

임베디드 시스템
컴퓨터와 메인 제어 보드를 함께 사용할 수 있도록 아두이노 우노를 라즈베리파이로 변경한다. 라즈베리파이 환경에 SW와 임베디드 시스템을 합쳐 베타오의 크기를 줄일 수 있다.

하드웨어
교육기관 도입 및 대량생산을 목표로 할 시, 하드웨어의 장치적 단순화 및 경량화가 가능해진다면 좀 더 유동적이고 용이한 작품으로 발전할 수 있다.

7. 사용한 제품 리스트

67_ICT_대상_오목AI 베타오(44)

8. 참고문헌
· 미니츄어 LM가이드 표준 블록/경예압.”미니츄어 모델의 정확한 치수 “.https://kr.misumi-ec.com/vona2/detail/110302586530/?KWSearch=lm%ea%b0%80%ec%9d%b4%eb%93%9c&searchFlow=results2products.(2021.03.13)
· 라즈이노 iOT.”[ 아두이노 기초 ] #31 스텝(Step Motor)모터의 이해.”스텝모터의 원리”.https://rasino.tistory.com/149.(2021.03.17)
· a4988 스텝 모터 드라이버 알아보기 ( 스테퍼 / 스테핑 / 모터 / 드라이버 / 초퍼 모터드라이버 / 설정 ).”스텝모터의 정격전압과 회로 구성 방법”.https://m.blog.naver.com/PostView.nhn?blogId=roboholic84&logNo=221142584042&proxyReferer=https:%2F%2Fwww.google.com%2F.(2021.03.18)
· 아두이노로 NEMA17 스텝모터 제어하기, 스텝수 계산 (A4988 스텝모터드라이버).”스텝모터의 분주 제어방법”.https://blog.naver.com/chrkwlq1208/221562820510.(2021.03.25)
· 오목을 두는 인공지능을 만들어 겨뤄봤습니다.오목인공지능 구현 방법.https://www.youtube.com/watch?v=xigPAOl3v7I&ab_channel=%EB%B9%B5%ED%98%95%EC%9D%98%EA%B0%9C%EB%B0%9C%EB%8F%84%EC%83%81%EA%B5%AD(21.03.10)
· GOMOCUP.”전처리데이터 다운로드”.https://gomocup.org/.(21.03.12)
· [아두이노 -> 파이썬]시리얼 통신으로 문자열 보내는 방법].”파이썬에서 아두이노로 시리얼 통신하는 방법”.https://doomed-lab.tistory.com/4.(21.03.20)
· 정성환, 배종욱(2005).OpenCV-Python으로 배우는 영상 처리 및 응용.생능출판사

9. 소스코드
아두이노 하드웨어 X축 방향 구동 코드

/* x축 stepping motor 구동 code */
String sig;
char Ard1[3] = {0}; // 통신 받은 문자열 분할 처리를 위한 변수 선언
char Ard2[3] = {0}; // 통신 받은 문자열 분할 처리를 위한 변수 선언
char check[1];
int value_1 = 0; // x축 좌표값 받을 변수 선언
int value_2 = 0; // y축 좌표값 받을 변수 선언
int flag = 0; // 통신 데이터의 무한한 공급을 막기 위한 변수 선언

/* stepping motor pin설정 */
#define steps_x 5
#define dir_x 6
#define ms1_x 8
#define ms2_x 9
#define ms3_x 10
/* switch pin설정 */
#define sw_x 4

long long int i = 0; // for문에 사용할 변수
//long long int i_num = 5000+4000*value_1;
long long int delay_num = (6000+4000*value_2)*0.4;
int sn = 0; // stepping motor가 회전하는 바퀴수를 count하기 위한 변수

void setup()
{
// put your setup code here, to run once:
Serial.begin(9600);
// 신호보낼 핀 출력설정
pinMode(steps_x, OUTPUT); // stepping motor에 신호를 주기 위함
pinMode(dir_x, OUTPUT); // stepping motor 방향설정을 출력
pinMode(ms1_x, OUTPUT); // stepping motor 분주 설정 출력
pinMode(ms2_x, OUTPUT); // stepping motor 분주 설정 출력
pinMode(ms3_x, OUTPUT); // stepping motor 분주 설정 출력
/* 분주를 1/2로 설정했으므로 1 step당 0.9도씩 회전 */
digitalWrite(steps_x,LOW);
digitalWrite(dir_x, HIGH); // 초기 설정 : stepping motor 정방향 회전
digitalWrite(ms1_x, HIGH); // stepping motor 1/2분주 설정
digitalWrite(ms2_x, LOW); // 분주 비활성화
digitalWrite(ms3_x, LOW); // 분주 비활성화

pinMode(sw_x, INPUT_PULLUP); // pull_up저항으로 출력 설정

/* x축의 stepping motor회전으로 초기 좌표까지 이동 */
digitalWrite(dir_x, HIGH); // stepping motor 정회전
while(1)
{
if(digitalRead(sw_x) == HIGH)
{
goto kn_s; // switch인식할 경우 kn_s:로 이동 및 stepping motor 정지
}
/* stepping motor 1 step당 4us로 회전 */
digitalWrite(steps_x, HIGH);
delayMicroseconds(200); // delay 2us
digitalWrite(steps_x, LOW);
delayMicroseconds(200); // delay 2us
}
kn_s:
delay(200);
}

void loop() {
// put your main code here, to run repeatedly:
digitalWrite(dir_x, HIGH); // stepping motor 정회전
/* x축의 stepping motor회전으로 초기 좌표까지 이동 */
while(1)
{
//x좌표 switch가 인식될 경우에 초기 좌표로 인식
if(digitalRead(sw_x) == HIGH)
{
goto kn_s; // switch인식할 경우 kn_s:로 이동 및 stepping motor 정지
}
/* stepping motor 1 step당 4us로 회전 */
digitalWrite(steps_x, HIGH);
delayMicroseconds(200); // delay 2us
digitalWrite(steps_x, LOW);
delayMicroseconds(200); // delay 2us
}
kn_s:
delay(200);
kn_x:
/* 문자열로 저장*/
while(Serial.available())
{
char wait = Serial.read(); // python serial 통신으로 받을 문자열
sig.concat(wait); // python으로 통신받은 문자열을 sig라는 빈 문자열에 추가
}
/* 입력 문자열 슬라이싱 */
sig.substring(0,1).toCharArray(check,2);

if(check[0] == ‘Q’)
{
if (sig.length()==5)
{
sig.substring(1,3).toCharArray(Ard1,3); // x축에 해당하는 좌표 문자열
sig.substring(3,5).toCharArray(Ard2,3); // y축에 해당하는 좌표 문자열
value_1 = atoi(Ard1); // x축 좌표값
value_2 = atoi(Ard2); // y축 좌표값
sig = “”; // x,y 축의 좌표데이터를 연산처리 할 수 있게 int형으로 처리한 후 문자열을 다시 받기 위한 초기화
flag = 0;
}
else if(sig.length()>5)
{sig = “”;} // 문자열을 다시 받기 위한 초기화
}
else if(check[0] != ‘Q’)
{
sig = “”; // 문자열을 다시 받기 위한 초기화
}
if(value_1 > 0) // x축 좌표가 0이상일 때 동작하기 위한 조건문
{
if(flag == 0)
{
flag = 1; // 통신 데이터의 무한 공급으로 인한 무한루프의 시스템 처리를 막기 위해 초기화
digitalWrite(dir_x, LOW);
delay(500);
/* x축 초기 좌표 위치 설정 */
for(i=0; i<6500; i++)
{
/* stepping motor 1 step당 4us로 회전 */
digitalWrite(steps_x, HIGH);
delayMicroseconds(200); // delay 2us
digitalWrite(steps_x, LOW);
delayMicroseconds(200); // delay 2us
}
sn = 0; // count 변수 초기화
/* y축 좌표값까지 한 바퀴씩 회전하는 값을 카운트하여 조건을 만족하면 회전 정지 및 while문 탈출 */
while(1)
{
if(sn == value_1)
{
goto ksn; // count변수가 x축 좌표값이랑 같아지면 stepping motor 회전 중지
}
for(i=0; i<4000; i++)
{
/* stepping motor 1 step당 4us로 회전 */
digitalWrite(steps_x, HIGH);
delayMicroseconds(200); // delay 2us
digitalWrite(steps_x, LOW);
delayMicroseconds(200); // delay 2us
}
sn++; // count 1씩 증가
}
ksn:
delay(delay_num); // y축 stepping motor 회전 시간
delay(13000); // servo & linear 작동 시간동안 delay
//회전방향 출력 10000+2000*
digitalWrite(dir_x, HIGH);
delay(500);
for(i=0; i<6500; i++)
{
/* stepping motor 1 step당 4us로 회전 */
digitalWrite(steps_x, HIGH);
delayMicroseconds(200); // delay 2us
digitalWrite(steps_x, LOW);
delayMicroseconds(200); // delay 2us
}
while(1)
{
for(i=0; i<4000; i++)
{
if(digitalRead(sw_x) == HIGH)
{
goto kn_x;
}
/* stepping motor 1 step당 4us로 회전 */
digitalWrite(steps_x, HIGH);
delayMicroseconds(200); // delay 2us
digitalWrite(steps_x, LOW);
delayMicroseconds(200); // delay 2us
}
}
}
}
}

아두이노 하드웨어 Y축 방향 구동 코드

/* y축 stepping motor & servo motor & linear motor 구동 code */
#include <Servo.h>
Servo myservo_servo; // servo motor사용을 위한 모터이름 지정
Servo myservo_linear; // linear motor사용을 위한 모터이름 지정

String sig; // 통신받을 문자열을 추가해주기 위한 자리 변수 선언
char Ard1[3] = {0}; // 통신 받은 문자열 분할 처리를 위한 변수 선언
char Ard2[3] = {0}; // 통신 받은 문자열 분할 처리를 위한 변수 선언
char check[1];
int value_1 = 0; // x축 좌표값 받을 변수 선언
int value_2 = 0; // y축 좌표값 받을 변수 선언
int flag = 0; // 통신 데이터의 무한한 공급을 막기 위한 변수 선언

/* stepping motor pin설정 */
#define steps_y 5
#define dir_y 6
#define ms1_y 8
#define ms2_y 9
#define ms3_y 10
/* switch pin설정 */
#define sw_y 4
long long int j = 0; // for문에 사용할 변수
int angle_servo; // servo motor 각도변수
int angle_linear; // linear motor 출력변수
long long int delay_num = (6500+4000*value_1)*0.4; //x축의 stepping motor회전하는 동안 동작지연하기 위한 delay
int sn = 0; // stepping motor가 회전하는 바퀴수를 count하기 위한 변수

void setup() {
// put your setup code here, to run once:
Serial.begin(9600);
// 신호보낼 핀 출력설정
pinMode(steps_y, OUTPUT); // stepping motor에 신호를 주기 위함
pinMode(dir_y, OUTPUT); // stepping motor 방향설정을 출력
pinMode(ms1_y, OUTPUT); // stepping motor 분주 설정 출력
pinMode(ms2_y, OUTPUT); // stepping motor 분주 설정 출력
pinMode(ms3_y, OUTPUT); // stepping motor 분주 설정 출력
/* 분주를 1/2로 설정했으므로 1 step당 0.9도씩 회전 */
digitalWrite(steps_y,LOW);
digitalWrite(dir_y, HIGH); // 초기 설정 : stepping motor 정방향 회전
digitalWrite(ms1_y, HIGH); // stepping motor 1/2분주 설정
digitalWrite(ms2_y, LOW); // 분주 비활성화
digitalWrite(ms3_y, LOW); // 분주 비활성화

myservo_servo.attach(11); // servo motor PIN설정
myservo_servo.write(0); // servo motor 초기 각도 0도로 설정
myservo_linear.attach(12); // linear motor PIN설정
myservo_linear.write(0); // linear motor 초기 각도 0도로 설정

pinMode(sw_y, INPUT_PULLUP); // pull_up저항으로 출력 설정

/* y축의 stepping motor회전으로 초기 좌표까지 이동 */
digitalWrite(dir_y, HIGH); // stepping motor 정방향 회전
while(1)
{
if(digitalRead(sw_y) == HIGH)
{
goto kn_s; // switch인식할 경우 kn_s:로 이동 및 stepping motor 정지
}
/* stepping motor 1 step당 4us로 회전 */
digitalWrite(steps_y, HIGH);
delayMicroseconds(200); // delay 2us
digitalWrite(steps_y, LOW);
delayMicroseconds(200); // delay 2us
}
kn_s:
delay(200);
}

void loop()
{
// put your main code here, to run repeatedly:
digitalWrite(dir_y, HIGH); // stepping motor 정회전
/* y축의 stepping motor회전으로 초기 좌표까지 이동 */
while(1)
{
if(digitalRead(sw_y) == HIGH)
{
goto kn_s;
}
/* stepping motor 1 step당 4us로 회전 */
digitalWrite(steps_y, HIGH);
delayMicroseconds(200); // delay 2us
digitalWrite(steps_y, LOW);
delayMicroseconds(200); // delay 2us
}
kn_s:
delay(200);
kn_y:
/* 문자열로 저장*/
while(Serial.available())
{
char wait = Serial.read(); // python serial 통신으로 받을 문자열
sig.concat(wait); // python으로 통신받은 문자열을 sig라는 빈 문자열에 추가
}
/* 입력 문자열 슬라이싱 */
sig.substring(0,1).toCharArray(check,2);

if(check[0] == ‘Q’)
{
if (sig.length()==5)
{
sig.substring(1,3).toCharArray(Ard1,3); // x축에 해당하는 좌표 문자열
sig.substring(3,5).toCharArray(Ard2,3); // y축에 해당하는 좌표 문자열
value_1 = atoi(Ard1); // x축 좌표값
value_2 = atoi(Ard2); // y축 좌표값
sig = “”; // x,y 축의 좌표데이터를 연산처리 할 수 있게 int형으로 처리한 후 문자열을 다시 받기 위한 초기화
flag = 0;
}
else if(sig.length()>5)
{sig = “”;} // 문자열을 다시 받기 위한 초기화
}
else if(check[0] != ‘Q’)
{
sig = “”; // 문자열을 다시 받기 위한 초기화
}
if(value_2 > 0) // y축 좌표가 0이상일 때 동작하기 위한 조건문
{
if(flag == 0)
{
flag = 1; // 통신 데이터의 무한 공급으로 인한 무한루프의 시스템 처리를 막기 위해 초기화
digitalWrite(dir_y, LOW);
delay(500);
delay(delay_num);
/* y축 초기 좌표 위치 설정 */
for(j=0; j<6000; j++)
{
/* stepping motor 1 step당 4us로 회전 */
digitalWrite(steps_y, HIGH);
delayMicroseconds(200); // delay 2us
digitalWrite(steps_y, LOW);
delayMicroseconds(200); // delay 2us
}
sn = 0; // count 변수 초기화
/* y축 좌표값까지 한 바퀴씩 회전하는 값을 카운트하여 조건을 만족하면 회전 정지 및 while문 탈출 */
while(1)
{
if(sn == value_2)
{
goto ksn; // count변수가 y축 좌표값이랑 같아지면 stepping motor 회전 중지
}
for(j=0; j<4000; j++)
{
/* stepping motor 1 step당 4us로 회전 */
digitalWrite(steps_y, HIGH);
delayMicroseconds(200); // delay 2us
digitalWrite(steps_y, LOW);
delayMicroseconds(200); // delay 2us
}
sn++; // count 1씩 증가
}
ksn:
delay(500);
/* 바둑돌이 정확한 위치로 낙하하기 위해 linear motor로 조준해주기 */
for(angle_linear = 0; angle_linear < 140; angle_linear++)
{
myservo_linear.write(angle_linear);
delay(20); // delay 10ms
}
/* 바둑돌 내려주기 위한 servo motor제어 0~180~0도 순으로 회전 */
delay(500);
for(angle_servo = 0; angle_servo <= 180; angle_servo++)
{
myservo_servo.write(angle_servo);
delay(10); // delay 10ms
}
delay(500);
for(angle_servo = 180; angle_servo >= 0; angle_servo–)
{
myservo_servo.write(angle_servo);
delay(10); // delay 10ms
}
delay(500);
/* 조준된 linear motor가 축이 움직일 때 다른 바둑돌을 건들지 않게 초기위치로 되돌림 */
for(angle_linear = 140; angle_linear > 0; angle_linear–)
{
myservo_linear.write(angle_linear);
delay(20); // delay 20ms
}
delay(delay_num);
digitalWrite(dir_y, HIGH); // stepping motor 정회전
delay(500);
for(j=0; j<6000; j++)
{
/* stepping motor 1 step당 4us로 회전 */
digitalWrite(steps_y, HIGH);
delayMicroseconds(200); // delay 2us
digitalWrite(steps_y, LOW);
delayMicroseconds(200); // delay 2us
}
while(1)
{
for(j=0; j<4000; j++)
{
if(digitalRead(sw_y) == HIGH)
{
goto kn_y; // y축의 switch가 눌리면 while문을 탈출하면서 stepping motor의 작동을 멈춤
}
/* stepping motor 1 step당 4us로 회전 */
digitalWrite(steps_y, HIGH);
delayMicroseconds(200); // delay 2us
digitalWrite(steps_y, LOW);
delayMicroseconds(200); // delay 2us
}
}
}

}
}

OpenCV 카메라 바둑돌 인식용 코드

import numpy as np, cv2
import time
image = cv2.imread(‘images/go2.jpg’, cv2.IMREAD_COLOR)
capture = cv2.VideoCapture(0) #카메라 불러오는 함수
if capture.isOpened() == False: raise Exception(“카메라 연결 안됨”)
stone = [] final_stone = [] while(capture.isOpened()): # 카메라가 열려있을 때 ~
ret, image = capture.read() # image로 받아옴

mark = np.zeros(image.shape, image.dtype) # mark 라는 친구를 생성함
mark_1 = np.zeros(image.shape, image.dtype) # mark_1이라는 친구도 생성함함
blue_threshold = 70 # 흑백으로 바꾸는 기준 1
green_threshold = 70 # 흑백으로 바꾸는 기준 2
red_threshold = 70 # 흑백으로 바꾸는 기준 3
bgr_threshold = [blue_threshold, green_threshold, red_threshold] # 3차월 배열로 바꿔줌

for i in range(image.shape[0]):
for j in range(image.shape[1]):
if (image[i, j, 0] >= blue_threshold) and (image[i, j, 1] >= green_threshold) and image[i, j, 2] >= red_threshold:
mark_1[i, j ] = 255,255,255 # 검정
else :
mark_1[i, j ] = 0,0,0 # 하얀색

ju = cv2.imread(‘images/go2.jpg’, cv2.COLOR_BGR2GRAY) # 검출할 사진 불러옴
white_s = cv2.cvtColor(ju, cv2.COLOR_BGR2GRAY) # 불러온 친구의 색 공간을 바꿈
mark = cv2.cvtColor(mark_1 , cv2.COLOR_BGR2GRAY) # 위와 동문
template = white_s # template = 비교할 것
w, h =template.shape[:: -1] # 불러온 사진의 너비와 높이

res = cv2.matchTemplate(mark, template, cv2.TM_CCOEFF_NORMED) # 함수를 사용해서 비교함
threshold = 0.6 # 이 값보다 높을 때
loc = np.where(res > threshold) # 위치를 리턴함
for pt in zip(*loc[::-1]): # 위치의 가로 세로
if (image[pt[1], pt[0],2] != 255):
cv2.rectangle(image, pt , (pt[0]+w, pt[1]+h), (0,255,0),2) # 초록색으로 칠함
cv2.circle(image, (int(pt[0]+w*0.5), int(pt[1]+h*0.5)),1, (255,0,0)) # 중심에 파란 원
s_x = int(pt[0] + w * 0.5) # 중심 x
s_y = int(pt[1] + h * 0.5) # 중심 y
s_xy = str(s_x) + str(s_y) # str로 합침
s_xy = int(s_xy) # 다시 숫자로 바꿈
s_xy_dt = [] stone_dt = [] for i in range(0, 10): # 범위 내 값을 걸러줌
for j in range(0, 10):
s_xy_dt.append(s_xy – i*1000 – j)
s_xy_dt.append(s_xy – i*1000 + j)
s_xy_dt.append(s_xy + i*1000 – j)
s_xy_dt.append(s_xy + i*1000 + j)
xx = 0
for k in range(len(s_xy_dt)): ## 값이 같지 않으면 stack
if stone != s_xy_dt[k]:
xx += 1
yy = 0
if xx == len(s_xy_dt) : # stack 값이 다 같을 때 = 오차의 범위 값이 list에 없을 때
if s_xy < 99999 : # y축이 두자리 프레임일 때
for k in range(len(stone)):
if abs(stone[k] – s_xy) < 110: # 100의 자리 = x축 프레임 차이, 1의 자리 = y축 프레임
yy += 1
if yy == 0 :
stone.append(s_xy)

else : # x ,y 둘다 3자리 프레임일 때
for k in range(len(stone)):
if abs(stone[k] – s_xy) < 1010: # 1000의 자리 = x축, 1의자리 = y축의 차이
yy += 1
if yy == 0 :
stone.append(s_xy)

aa = len(final_stone)
for i in range(len(stone)):
stone_xx_1 = 0
stone_yy_1 = 0
if stone[i] < 99999 :
stone_x = stone[i] / 100 # 앞자리 3개
stone_y = stone[i] % 100 # 뒷자리 2개

stone_y = abs(stone_y) / 20 # 경험적 치수
stone_x = (abs((stone_x – 160) / 20) ) # 경험적 치수
else :
stone_x = stone[i] / 1000 # 앞자리 3개
stone_y = stone[i] % 1000 # 뒷자리 3개

stone_x = (abs((stone_x – 160) / 20)) # 경험적 치수
stone_y = (stone_y / 20 ) # 경험적 치수
ab = int(stone_x) *100 + int(stone_y)
if ab not in final_stone: # list에 없으면
final_stone.append(ab)

if aa != len(final_stone) : # 전의 길이와 다르면 = 새롭게 추가되면
print( int(final_stone[-1] % 100), int(final_stone[-1]/100))
print(‘——————’)
cv2.imshow(‘mm’, image) # 이미지 출력
cv2.imshow(‘aa’, ju) # 비교 이미지 출력
cv2.waitKey(150) # delay
5neck.py
import numpy as np, cv2
import time
image = cv2.imread(‘images/go2.jpg’, cv2.IMREAD_COLOR)
capture = cv2.VideoCapture(0) #카메라 불러오는 함수
if capture.isOpened() == False: raise Exception(“카메라 연결 안됨”)
stone = [] final_stone = [] while(capture.isOpened()): # 카메라가 열려있을 때 ~
ret, image = capture.read() # image로 받아옴

mark = np.zeros(image.shape, image.dtype) # mark 라는 친구를 생성함
mark_1 = np.zeros(image.shape, image.dtype) # mark_1이라는 친구도 생성함함
blue_threshold = 70 # 흑백으로 바꾸는 기준 1
green_threshold = 70 # 흑백으로 바꾸는 기준 2
red_threshold = 70 # 흑백으로 바꾸는 기준 3
bgr_threshold = [blue_threshold, green_threshold, red_threshold] # 3차월 배열로 바꿔줌

for i in range(image.shape[0]):
for j in range(image.shape[1]):
if (image[i, j, 0] >= blue_threshold) and (image[i, j, 1] >= green_threshold) and image[i, j, 2] >= red_threshold:
mark_1[i, j ] = 255,255,255
else :
mark_1[i, j ] = 0,0,0

#print(mark.shape)

ju = cv2.imread(‘images/go2.jpg’, cv2.COLOR_BGR2GRAY) # 검출할 사진 불러옴
white_s = cv2.cvtColor(ju, cv2.COLOR_BGR2GRAY) # 불러온 친구의 색 공간을 바꿈
mark = cv2.cvtColor(mark_1 , cv2.COLOR_BGR2GRAY) # 위와 동문
template = white_s # template = 비교할 것
w, h =template.shape[:: -1] # 불러온 사진의 너비와 높이
#stone = [] res = cv2.matchTemplate(mark, template, cv2.TM_CCOEFF_NORMED) # 함수를 사용해서 비교함
threshold = 0.6 # 이 값보다 높을 때
loc = np.where(res > threshold) # 위치를 리턴함
for pt in zip(*loc[::-1]): # 위치의 가로 세로
if (image[pt[1], pt[0],2] != 255):
cv2.rectangle(image, pt , (pt[0]+w, pt[1]+h), (0,255,0),2) # 초록색으로 칠함
cv2.circle(image, (int(pt[0]+w*0.5), int(pt[1]+h*0.5)),1, (255,0,0))
#print(int(pt[0]+w*0.5), int(pt[1]+h*0.5))
s_x = int(pt[0] + w * 0.5)
s_y = int(pt[1] + h * 0.5)
s_xy = str(s_x) + str(s_y)
s_xy = int(s_xy)
s_xy_dt = [] stone_dt = [] for i in range(0, 10):
for j in range(0, 10):
s_xy_dt.append(s_xy – i*1000 – j)
s_xy_dt.append(s_xy – i*1000 + j)
s_xy_dt.append(s_xy + i*1000 – j)
s_xy_dt.append(s_xy + i*1000 + j)
xx = 0
for k in range(len(s_xy_dt)):
if stone != s_xy_dt[k]:
xx += 1
yy = 0
if xx == len(s_xy_dt) :
if s_xy < 99999 :
for k in range(len(stone)):
if abs(stone[k] – s_xy) < 110:
yy += 1
if yy == 0 :
stone.append(s_xy)
else :
for k in range(len(stone)):
if abs(stone[k] – s_xy) < 1010:
yy += 1
if yy == 0 :
stone.append(s_xy)
#print(stone)
aa = len(final_stone)
for i in range(len(stone)):
stone_xx_1 = 0
stone_yy_1 = 0
if stone[i] < 99999 :
stone_x = stone[i] / 100
stone_y = stone[i] % 100
#print(stone_x)
#print(stone_y)
stone_y = abs(stone_y) / 20
stone_x = (abs((stone_x – 160) / 20) )
else :
stone_x = stone[i] / 1000
stone_y = stone[i] % 1000
#print(stone_x)
#print(stone_y)
stone_x = (abs((stone_x – 160) / 20))

stone_y = (stone_y / 20 )
ab = int(stone_x) *100 + int(stone_y)
if ab not in final_stone:
final_stone.append(ab)

#for k in range(len(final_stone)):
# print(int(final_stone[k] / 100) , int(final_stone[k] % 100))
if aa != len(final_stone) :
print( int(final_stone[-1] % 100), int(final_stone[-1]/100))
print(‘——————’)
cv2.imshow(‘mm’, image)
cv2.imshow(‘aa’, ju)
cv2.waitKey(150)

 

데이터 전처리 코드

import numpy as np
from glob import glob
from tqdm import tqdm # for 문의 상태바를 나태내주는 라이브러리
import os

”’
Dataset from https://gomocup.org/results/
데이터셋을 얻는 프로그램
# 데이터 전처리 과정
game_rule = ‘Freestyle’ # 프리스타일(3×3가능 5개 이상만 돌을 연속으로 두면 승리)
base_path = ‘/omok_AI/gomocup2020results’ # 전처리전 데이터가 저장되어 있는 경로

output_path = os.path.join(‘dataset’, os.path.basename(base_path)) # dataset\gomocup2019results\
os.makedirs(output_path, exist_ok=True) # 디렉토리 생성

# 경로에 있는 모든 psq파일을 file_list에 저장
file_list = glob(os.path.join(base_path, ‘%s*/*.psq’ % (game_rule, )))

for index, file_path in enumerate(tqdm(file_list)):
with open(file_path, ‘r’) as f:
lines = f.read().splitlines()

w, h = lines[0].split(‘ ‘)[1].strip(‘,’).split(‘x’) # 가로 X 세로의 크기 추출
w, h = int(w), int(h) # 문자에서 인트형으로 형 변환

lines = lines[1:] # 첫번째 줄을 제외한 나머지 줄

inputs, outputs = [], [] board = np.zeros([h, w], dtype=np.int8)

for i, line in enumerate(lines):
if ‘,’ not in line:
break

x, y, t = np.array(line.split(‘,’), np.int8)
if i % 2 == 0: # 오목의 순서제
player = 1
else:
player = 2

input = board.copy().astype(np.int8)
input[(input != player) & (input != 0)] = -1
input[(input == player) & (input != 0)] = 1

output = np.zeros([h, w], dtype=np.int8)
output[y-1, x-1] = 1 # h w 순서

# augmentation
# rotate 4 x flip 3 = 12
# 데이터셋을 늘림
for k in range(4):
input_rot = np.rot90(input, k=k)
output_rot = np.rot90(output, k=k)
inputs.append(input_rot)
outputs.append(output_rot)
inputs.append(np.fliplr(input_rot))
outputs.append(np.fliplr(output_rot))
inputs.append(np.flipud(input_rot))
outputs.append(np.flipud(output_rot))
# update board
board[y-1, x-1] = player
# dataset 저장
np.savez_compressed(os.path.join(output_path, ‘%s.npz’ % (str(index).zfill(5))), inputs=inputs, outputs=outputs)

Execute_GUI.py

from ursina import *
import numpy as np
from tensorflow.keras.models import load_model
from omok import Board, Omok
import serial
import time
# from serial_arduino import coordinate_str

PORT = ‘COM13′
BaudRate = 9600
sendSerial = serial.Serial(‘COM13′, BaudRate) # x축
ardSerial = serial.Serial(‘COM11′, 9600) # y축

# sendSerial = serial.Serial(‘COM14′, BaudRate)

# 오목 6개 만들시에도 게임을 승리하는 부분 보완
model = load_model(‘models/20210307_232530.h5′)

app = Ursina()
window.borderless = False
window.color = color._50

w, h = 20, 20
camera.orthographic = True
camera.fov = 23
camera.position = (w//2, h//2)

board = Board(w=w, h=h)
board_buttons = [[None for x in range(w)] for y in range(h)] game = Omok(board=board)

Entity(model=Grid(w+1, h+1), scale=w+1, color=color.blue, x=w//2-0.5, y=h//2-0.5, z=0.1)

for y in range(h):
for x in range(w):
b = Button(parent=scene, position=(x, y), color=color.clear, model=’circle’, scale=0.9) # 마우스 왼쪽 클릭
board_buttons[y][x] = b

def on_mouse_enter(b=b):
if b.collision:
b.color = color._100
def on_mouse_exit(b=b):
if b.collision:
b.color = color.clear

b.on_mouse_enter = on_mouse_enter
b.on_mouse_exit = on_mouse_exit

def on_click(b=b):
# player turn
b.text = ’1′
b.color = color.black
b.collision = False

game.put(x=int(b.position.x), y=int(h – b.position.y – 1)) # start from top left
won_player = game.check_won()

if won_player > 0:
end_session(won_player)
game.next()

# cpu turn
input_o = game.board.board.copy()
input_o[(input_o != 1) & (input_o != 0)] = -1
input_o[(input_o == 1) & (input_o != 0)] = 1
input_o = np.expand_dims(input_o, axis=(0, -1)).astype(np.float32)

output = model.predict(input_o).squeeze()
output = output.reshape((h, w))
output_y, output_x = np.unravel_index(np.argmax(output), output.shape)
game.put(x=output_x, y=output_y)

data_x = str(output_x)
data_y = str(output_y)
print(data_x, data_y)

# 아두이노에 넘겨줄 데이터 정제
# 00/00 의 형태로 data_output 생성
if len(data_x) != 2:
data_x = ’0′ + data_x
if len(data_y) != 2:
data_y = ’0′ + data_y
data_output = “Q” + data_x + data_y
data_output = data_output.encode(‘utf-8′)

sendSerial.write(data_output)
ardSerial.write(data_output)

print(type(data_output))
print(data_output)

board_buttons[h - output_y - 1][output_x].text = ’2′
board_buttons[h - output_y - 1][output_x].text_color = color.black
board_buttons[h - output_y - 1][output_x].color = color.white
board_buttons[h - output_y - 1][output_x].collision = False

won_player = game.check_won()

if won_player > 0:
end_session(won_player)

game.next()
print(game.board)
b.on_click = on_click
def end_session(won_player):
Panel(z=1, scale=10, model=’quad’)
t = Text(f’Player {won_player} won!’, scale=3, origin=(0, 0), background=True)
t.create_background(padding=(.5,.25), radius=Text.size/2)

app.run()

영상인식 코드

import numpy as np, cv2
import time
image = cv2.imread(‘images/go2.jpg’, cv2.IMREAD_COLOR)
capture = cv2.VideoCapture(0) #카메라 불러오는 함수
if capture.isOpened() == False: raise Exception(“카메라 연결 안됨”)
stone = [] final_stone = [] while(capture.isOpened()): # 카메라가 열려있을 때 ~
ret, image = capture.read() # image로 받아옴

mark = np.zeros(image.shape, image.dtype) # mark 라는 친구를 생성함
mark_1 = np.zeros(image.shape, image.dtype) # mark_1이라는 친구도 생성함함
blue_threshold = 70 # 흑백으로 바꾸는 기준 1
green_threshold = 70 # 흑백으로 바꾸는 기준 2
red_threshold = 70 # 흑백으로 바꾸는 기준 3
bgr_threshold = [blue_threshold, green_threshold, red_threshold] # 3차월 배열로 바꿔줌

for i in range(image.shape[0]):
for j in range(image.shape[1]):
if (image[i, j, 0] >= blue_threshold) and (image[i, j, 1] >= green_threshold) and image[i, j, 2] >= red_threshold:
mark_1[i, j ] = 255,255,255 # 검정
else :
mark_1[i, j ] = 0,0,0 # 하얀색

ju = cv2.imread(‘images/go2.jpg’, cv2.COLOR_BGR2GRAY) # 검출할 사진 불러옴
white_s = cv2.cvtColor(ju, cv2.COLOR_BGR2GRAY) # 불러온 친구의 색 공간을 바꿈
mark = cv2.cvtColor(mark_1 , cv2.COLOR_BGR2GRAY) # 위와 동문
template = white_s # template = 비교할 것
w, h =template.shape[:: -1] # 불러온 사진의 너비와 높이

res = cv2.matchTemplate(mark, template, cv2.TM_CCOEFF_NORMED) # 함수를 사용해서 비교함
threshold = 0.6 # 이 값보다 높을 때
loc = np.where(res > threshold) # 위치를 리턴함
for pt in zip(*loc[::-1]): # 위치의 가로 세로
if (image[pt[1], pt[0],2] != 255):
cv2.rectangle(image, pt , (pt[0]+w, pt[1]+h), (0,255,0),2) # 초록색으로 칠함
cv2.circle(image, (int(pt[0]+w*0.5), int(pt[1]+h*0.5)),1, (255,0,0)) # 중심에 파란 원
s_x = int(pt[0] + w * 0.5) # 중심 x
s_y = int(pt[1] + h * 0.5) # 중심 y
s_xy = str(s_x) + str(s_y) # str로 합침
s_xy = int(s_xy) # 다시 숫자로 바꿈
s_xy_dt = [] stone_dt = [] for i in range(0, 10): # 범위 내 값을 걸러줌
for j in range(0, 10):
s_xy_dt.append(s_xy – i*1000 – j)
s_xy_dt.append(s_xy – i*1000 + j)
s_xy_dt.append(s_xy + i*1000 – j)
s_xy_dt.append(s_xy + i*1000 + j)
xx = 0
for k in range(len(s_xy_dt)): ## 값이 같지 않으면 stack
if stone != s_xy_dt[k]:
xx += 1
yy = 0
if xx == len(s_xy_dt) : # stack 값이 다 같을 때 = 오차의 범위 값이 list에 없을 때
if s_xy < 99999 : # y축이 두자리 프레임일 때
for k in range(len(stone)):
if abs(stone[k] – s_xy) < 110: # 100의 자리 = x축 프레임 차이, 1의 자리 = y축 프레임
yy += 1
if yy == 0 :
stone.append(s_xy)

else : # x ,y 둘다 3자리 프레임일 때
for k in range(len(stone)):
if abs(stone[k] – s_xy) < 1010: # 1000의 자리 = x축, 1의자리 = y축의 차이
yy += 1
if yy == 0 :
stone.append(s_xy)

aa = len(final_stone)
for i in range(len(stone)):
stone_xx_1 = 0
stone_yy_1 = 0
if stone[i] < 99999 :
stone_x = stone[i] / 100 # 앞자리 3개
stone_y = stone[i] % 100 # 뒷자리 2개

stone_y = abs(stone_y) / 20 # 경험적 치수
stone_x = (abs((stone_x – 160) / 20) ) # 경험적 치수
else :
stone_x = stone[i] / 1000 # 앞자리 3개
stone_y = stone[i] % 1000 # 뒷자리 3개

stone_x = (abs((stone_x – 160) / 20)) # 경험적 치수
stone_y = (stone_y / 20 ) # 경험적 치수
ab = int(stone_x) *100 + int(stone_y)
if ab not in final_stone: # list에 없으면
final_stone.append(ab)

if aa != len(final_stone) : # 전의 길이와 다르면 = 새롭게 추가되면
print( int(final_stone[-1] % 100), int(final_stone[-1]/100))
print(‘——————’)
cv2.imshow(‘mm’, image) # 이미지 출력
cv2.imshow(‘aa’, ju) # 비교 이미지 출력
cv2.waitKey(150) # delay
5neck.py
import numpy as np, cv2
import time
image = cv2.imread(‘images/go2.jpg’, cv2.IMREAD_COLOR)
capture = cv2.VideoCapture(0) #카메라 불러오는 함수
if capture.isOpened() == False: raise Exception(“카메라 연결 안됨”)
stone = [] final_stone = [] while(capture.isOpened()): # 카메라가 열려있을 때 ~
ret, image = capture.read() # image로 받아옴

mark = np.zeros(image.shape, image.dtype) # mark 라는 친구를 생성함
mark_1 = np.zeros(image.shape, image.dtype) # mark_1이라는 친구도 생성함함
blue_threshold = 70 # 흑백으로 바꾸는 기준 1
green_threshold = 70 # 흑백으로 바꾸는 기준 2
red_threshold = 70 # 흑백으로 바꾸는 기준 3
bgr_threshold = [blue_threshold, green_threshold, red_threshold] # 3차월 배열로 바꿔줌

for i in range(image.shape[0]):
for j in range(image.shape[1]):
if (image[i, j, 0] >= blue_threshold) and (image[i, j, 1] >= green_threshold) and image[i, j, 2] >= red_threshold:
mark_1[i, j ] = 255,255,255
else :
mark_1[i, j ] = 0,0,0

#print(mark.shape)

ju = cv2.imread(‘images/go2.jpg’, cv2.COLOR_BGR2GRAY) # 검출할 사진 불러옴
white_s = cv2.cvtColor(ju, cv2.COLOR_BGR2GRAY) # 불러온 친구의 색 공간을 바꿈
mark = cv2.cvtColor(mark_1 , cv2.COLOR_BGR2GRAY) # 위와 동문
template = white_s # template = 비교할 것
w, h =template.shape[:: -1] # 불러온 사진의 너비와 높이
#stone = [] res = cv2.matchTemplate(mark, template, cv2.TM_CCOEFF_NORMED) # 함수를 사용해서 비교함
threshold = 0.6 # 이 값보다 높을 때
loc = np.where(res > threshold) # 위치를 리턴함
for pt in zip(*loc[::-1]): # 위치의 가로 세로
if (image[pt[1], pt[0],2] != 255):
cv2.rectangle(image, pt , (pt[0]+w, pt[1]+h), (0,255,0),2) # 초록색으로 칠함
cv2.circle(image, (int(pt[0]+w*0.5), int(pt[1]+h*0.5)),1, (255,0,0))
#print(int(pt[0]+w*0.5), int(pt[1]+h*0.5))
s_x = int(pt[0] + w * 0.5)
s_y = int(pt[1] + h * 0.5)
s_xy = str(s_x) + str(s_y)
s_xy = int(s_xy)
s_xy_dt = [] stone_dt = [] for i in range(0, 10):
for j in range(0, 10):
s_xy_dt.append(s_xy – i*1000 – j)
s_xy_dt.append(s_xy – i*1000 + j)
s_xy_dt.append(s_xy + i*1000 – j)
s_xy_dt.append(s_xy + i*1000 + j)
xx = 0
for k in range(len(s_xy_dt)):
if stone != s_xy_dt[k]:
xx += 1
yy = 0
if xx == len(s_xy_dt) :
if s_xy < 99999 :
for k in range(len(stone)):
if abs(stone[k] – s_xy) < 110:
yy += 1
if yy == 0 :
stone.append(s_xy)
else :
for k in range(len(stone)):
if abs(stone[k] – s_xy) < 1010:
yy += 1
if yy == 0 :
stone.append(s_xy)
#print(stone)
aa = len(final_stone)
for i in range(len(stone)):
stone_xx_1 = 0
stone_yy_1 = 0
if stone[i] < 99999 :
stone_x = stone[i] / 100
stone_y = stone[i] % 100
#print(stone_x)
#print(stone_y)
stone_y = abs(stone_y) / 20
stone_x = (abs((stone_x – 160) / 20) )
else :
stone_x = stone[i] / 1000
stone_y = stone[i] % 1000
#print(stone_x)
#print(stone_y)
stone_x = (abs((stone_x – 160) / 20))

stone_y = (stone_y / 20 )
ab = int(stone_x) *100 + int(stone_y)
if ab not in final_stone:
final_stone.append(ab)

#for k in range(len(final_stone)):
# print(int(final_stone[k] / 100) , int(final_stone[k] % 100))
if aa != len(final_stone) :
print( int(final_stone[-1] % 100), int(final_stone[-1]/100))
print(‘——————’)
cv2.imshow(‘mm’, image)
cv2.imshow(‘aa’, ju)
cv2.waitKey(150)

 

Omok.py

import numpy as np
class Board():
def __init__(self, w, h):
self.w = w
self.h = h
self.board = np.zeros((self.h, self.w), dtype=np.int)
“”"
def __repr__(self):
string = ”
data1 = [] data2 = [] for y in range(self.h):
for x in range(self.w):
string += ‘%d ‘ % self.board[y][x] if self.board[y][x] == 2:
data1.append(x)
data2.append(y)
string += ‘\n’

return string
“”"

class Omok():
def __init__(self, board):
self.board = board
self.current_player = 1
self.won_player = 0

def reset(self):
self.board.board = 0
self.current_player = 1
self.won_player = 0

def put(self, x=None, y=None):
if x is None and y is None:
while True:
rand_x = np.random.randint(0, self.board.w)
rand_y = np.random.randint(0, self.board.h)

if self.board.board[rand_y][rand_x] == 0:
self.board.board[rand_y][rand_x] = self.current_player
break
else:
self.board.board[y][x] = self.current_player

def next(self):
if self.current_player == 1:
self.current_player = 2
else:
self.current_player = 1

def check_won(self):
player = self.current_player

for y in range(self.board.h):
for x in range(self.board.w):
try:
if self.board.board[y][x] == player and self.board.board[y + 1][x] == player and \
self.board.board[y + 2][x] == player and self.board.board[y + 3][x] == player and \
self.board.board[y + 4][x] == player:
self.won_player = player
break
except:
pass

try:
if self.board.board[y][x] == player and self.board.board[y][x + 1] == player and \
self.board.board[y][x + 2] == player and self.board.board[y][x + 3] == player and \
self.board.board[y][x + 4] == player:
self.won_player = player
break
except:
pass

try:
if self.board.board[y][x] == player and self.board.board[y + 1][x + 1] == player and \
self.board.board[y + 2][x + 2] == player and self.board.board[y + 3][x + 3] == player and \
self.board.board[y + 4][x + 4] == player:
self.won_player = player
break
except:
pass

try:
if x >= 4 and self.board.board[y][x] == player and self.board.board[y + 1][x - 1] == player and \
self.board.board[y + 2][x - 2] == player and self.board.board[y + 3][x - 3] == player and \
self.board.board[y + 4][x - 4] == player:
self.won_player = player
break
except:
pass
if self.won_player > 0:
break
return self.won_player

 

모델 학습 코드

import numpy as np
import tensorflow as tf
import tensorflow.keras as keras
from tensorflow.keras import layers, models
from sklearn.model_selection import train_test_split
from glob import glob
from tqdm import tqdm
from datetime import datetime
from tensorflow.keras.callbacks import ModelCheckpoint, ReduceLROnPlateau
import os
w, h = 20, 20

file_list = glob(‘C:\JupyterProject\dataset_2020\*.npz’)

x_data, y_data = [], [] for file_path in tqdm(file_list):
data = np.load(file_path)
x_data.extend(data['inputs'])
y_data.extend(data['outputs'])
x_data = np.array(x_data, np.float32).reshape((-1, h, w, 1))
y_data = np.array(y_data, np.float32).reshape((-1, h * w))

x_train, x_val, y_train, y_val = train_test_split(x_data, y_data, test_size=0.2, random_state=2020)

del x_data, y_data

print(x_train.shape, y_train.shape)
print(x_val.shape, y_val.shape)
model = models.Sequential([
layers.Conv2D(64, 7, activation='relu', padding='same', input_shape=(h, w, 1)),
layers.Conv2D(128, 7, activation='relu', padding='same'),
layers.Conv2D(256, 7, activation='relu', padding='same'),
layers.Conv2D(128, 7, activation='relu', padding='same'),
layers.Conv2D(64, 7, activation='relu', padding='same'),
layers.Conv2D(1, 1, activation=None, padding='same'),
layers.Reshape((h * w,)),
layers.Activation('sigmoid')
])

model.compile(
optimizer=’adam’,
loss=’binary_crossentropy’,
metrics=['acc'] )
model.summary()

start_time = datetime.now().strftime(‘%Y%m%d_%H%M%S’)
os.makedirs(‘models’, exist_ok=True)

model.fit(
x=x_train,
y=y_train,
batch_size=256,
epochs=10,
callbacks=[
ModelCheckpoint('./models/%s.h5' % (start_time), monitor='val_acc', verbose=1, save_best_only=True, mode='auto'),
ReduceLROnPlateau(monitor='val_acc', factor=0.2, patience=5, verbose=1, mode='auto')
],
validation_data=(x_val, y_val),
use_multiprocessing=True,
workers=16
)

 

 

 

 

Leave A Comment

*