STORY
MinJ is_

2021년

<Q-learning> 세종 캠퍼스 내 최적 경로 안내 서비스

KMinJis 2022. 8. 1. 04:20

3학년 2학기에 학과 친구들과 함께 교내 해커톤 대회에 참가하였다.

첫 해커톤 대회인 만큼 기대도 되고, 걱정도 많이 되었다.

진행방식은 주최측에서 여러가지 주제를 제시해주면 우리가 그 중 한 개를 골라 구현시키는 것이였다.

우리는 "세종 캠퍼스 내 타건물 이동 시 최적 경로 안내 서비스"를 선택했다.

 

<STEP1> 프로젝트 개요

 목표는 세종대 내 건물들을 이동할 때 가장 빠른 최적의 경로를 찾아주는 AI를 구현하고, 해당 정보를 앱으로 보여주는 것이었다.

 네이버 지도로 세종대의 '광개토관'으로 부터 '진관홀'로 이동하려고 하면 아래의 그림처럼 5분이나 걸리는 길을 제시해준다. 하지만 학교 구조를 잘 아는 우리는 더 빠른 다른 길이 있음을 안다. 우리가 아는 정보를 바탕으로 최적 경로를 제시해줄 수 있다면, 처음이거나 아직 학교 구조를 모르는 사람들이 손쉽게 길을 찾아갈 수 있을 것이다.

 

<STEP2> 학교 건물 약도와 입구 및 길의 모서리 약도 그리기

먼저, 우리는 학교 건물과 길을 확인할 수 있는 약도를 그렸다.

직접 학교를 돌아다녀보며, 숨겨져 있는 문이나 길을 파악해서 그렸다.

* 초록색 - 한 도로의 양쪽 끝          * 빨간색 - 건물 입구              *보라색 - 건물

 

<STEP3> Open CV를 활용한 약도 인식

초록색과 빨간색의 원을 구분짓기 위해 두 색의 원은 크기를 다르게 그려주었다.

Open CV에는 크기별 원을 추출하는 함수와 외곽선을 추출하는 함수가 있어, 이를 활용해 약도에서의 외곽선, 초록색 원, 빨간색 원의 좌표 정보를 얻었다.

import cv2
from google.colab.patches import cv_imshow

src = cv2.imread("/content/gdrive/MyDrive/AI-SW-HACK/only_purple_map.png") #원본 이미지
#cv_imshow(src)
dst = src.copy()
gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)

ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
binary = cv2.bitwise_not(binary)

contours, hierarchy = cv2.findContours(binary, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)

for i in range(len(contours)):
    cv2.drawContours(dst, [contours[i]], 0, (0, 0, 0), 2)
    #cv2.putText(dst, str(i), tuple(contours[i][0][0]), cv2.FONT_HERSHEY_COMPLEX, 0.8, (0, 0, 0), 2)
    print(i, hierarchy[0][i])
    
    #cv2.waitKey(0)

cv_imshow(dst)


import cv2
from google.colab.patches import cv_imshow

src2 = cv2.imread("/content/gdrive/MyDrive/AI-SW-HACK/purple_map3.png") #원본 이미지
#cv_imshow(src2)
dst2 = src2.copy()
gray2 = cv2.cvtColor(src2, cv2.COLOR_BGR2GRAY)

circles = cv2.HoughCircles(gray2, cv2.HOUGH_GRADIENT, 1, 10, param1 = 50, param2 = 21, minRadius = 13, maxRadius = 17)
circles2 = cv2.HoughCircles(gray2, cv2.HOUGH_GRADIENT, 1, 5, param1 = 25, param2 = 10, minRadius = 7, maxRadius = 10)
print(circles[0])
print(circles2[0])
for i in circles[0]:
    cv2.circle(dst2, (i[0], i[1]), i[2], (0, 255, 0), 2)

for i in circles2[0]:
    cv2.circle(dst2, (i[0], i[1]), i[2], (255, 0, 0), 2)
    

cv_imshow(dst2)

위의 코드를 실행시킨 결과 아래의 사진처럼 제대로 인식되었음을 확인할 수 있었다.

 

<STEP4> Q-learning 학습 전 필요한 정보 csv 구축

 Q-learning을 공부하여 직접 코드를 구현하려고 했으나, 해커톤의 특성상 짧은 시간 안에 처음 배우는 AI 학습 방법을 구현하는 것은 어려움이 있었다. 하여 Q-learning 부분은 오픈 소스를 사용하기로 했고 해당 코드를 공부하여 알고리즘을 이해하고, 해당 코드에 적용시킬 수 있게 우리의 정보를 전처리하는 것이 목표가 되었다.

 사용할 오픈 소스를 확인해본 결과, 지나갈 수 있는 길의 출발점 좌표, 도착점 좌표, 두 점 사이의 거리값을 csv에 저장시켜 주어야 했다. 

 두 점 사이를 이었을 때 보라색을 지나지 않아야 사람이 지나갈 수 있는 길이 된다. 이러한 길만 정보에 담을 수 있게 코드를 구현하여야 했는데 해결 아이디어가 떠오르지 않아 한동안 애를 먹었다.

 여러 아이디어를 공유하며 시도한 끝에, 해결책을 생각해냈다.

 두 점을 이은 사진에서 외곽선의 갯수를 추출하여 처음의 외곽선의 갯수와 비교를 한다. 만일 두 점을 이은 사진에서의 외곽선의 갯수가 더 많다면, 두 점을 이으므로써 보라색 도형이 쪼개져 외곽선의 갯수가 늘어나는 것이므로 해당 길은 사람이 지나갈 수 없는 길로 판단하고 정보에 담지 않았다.

 이러한 방법으로 사람이 지나갈 수 있는 길을 추출해 흰색 선으로 이미지에 보이게 그려줬더니, 아래의 사진처럼 잘 추출된 것을 확인할 수 있었다.

그리고 아래는 사람의 지나갈 수 있는 길을 추출하고, Q-learning 오픈 소스에 적용시키기 위해 <STEP3>에서 얻은 정보를 사용하여 알맞은 형태로 csv에 담아주는 코드이다. 

all_circle = []

for i in range(len(circles[0])):
  tmp = [int(circles[0][i][0]),int(circles[0][i][1])]
  all_circle.append(tmp)
for i in range(len(circles2[0])):
  tmp = [int(circles2[0][i][0]),int(circles2[0][i][1])]
  all_circle.append(tmp)  

#print(circles[0])
#print(len(circles2[0]))
print(all_circle)

############## 지금까지 구한 리스트 정보 정리 ##############

#circles --> 건물 노드 좌표
#circles2 --> 도로 노드 좌표

#circles[0][i][0] : i번째 건물 노드의 중심의 x좌표
#circles[0][i][1] : i번째 건물 노드의 중심의 y좌표

#all_circle --> 모든 노드(건물+도로) 좌표
#all_circle[i][0] : i번째 노드의 중심의 x좌표
#all_circle[i][1] : i번째 노드의 중심의 y좌표

#len(contours) : 외곽선의 갯수
#contours[i] : i번째 도형의 외곽선
#contours[i][j][0][0] : i번째 도형의 외곽선의 점들 중 j번째 점의 x좌표
#contours[i][j][0][1] : i번째 도형의 외곽선의 점들 중 j번째 점의 y좌표

 for k in range(len(contours)):
   #print('______________________')
   for m in range(len(contours[k])):
     x = contours[k][m][0][0]
     y = contours[k][m][0][1]
     #print(x , y)
     
     import math

# 두 점 사이의 거리 구하는 함수
def distance(x1, y1, x2, y2):
    result = math.sqrt( math.pow(x1 - x2, 2) + math.pow(y1 - y2, 2))
    return result
    
#original 출발점, connected 도착점 weight 사이 거리값

original = []
connected = []
weight = []

for i in range(len(all_circle)):
  for j in range(i+1,len(all_circle)):
    dst3 = src.copy()
    dst3 = cv2.line(dst3, (all_circle[i][0],all_circle[i][1]), (all_circle[j][0],all_circle[j][1]), (255,255,255), 8, 8, 0)
    gray3 = cv2.cvtColor(dst3, cv2.COLOR_BGR2GRAY)
    ret3, binary3 = cv2.threshold(gray3, 127, 255, cv2.THRESH_BINARY)
    binary3 = cv2.bitwise_not(binary3)
    contours3, hierarchy3 = cv2.findContours(binary3, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
    if(len(contours3)>len(contours)):
      continue
    else:
      dst2 = cv2.line(dst2, (all_circle[i][0],all_circle[i][1]), (all_circle[j][0],all_circle[j][1]), (255,255,255), 1, 8, 0)
      # csv를 만들기 위한 리스트 구현
      ori = int(i)
      con = int(j)
      wei = distance(all_circle[i][0],all_circle[i][1],all_circle[j][0],all_circle[j][1])
      original.append(ori)
      connected.append(con)
      weight.append(wei)

cv_imshow(dst2)

#추출한 선 정보 리스트 3개 --> pd --> csv

# list --> pdf(pandas frame)
import pandas as pd

tmp_pdf = {
    'original':original,
    'connected':connected,
    'weight':weight
}

pdf = pd.DataFrame(tmp_pdf)

print(pdf)

# pdf --> csv
# 구글 드라이브 내 AI-SW-HACK 폴더에 gragh라는 이름으로 csv 저장
pdf.to_csv('/content/gdrive/MyDrive/AI-SW-HACK/graph.csv',header=True, index=False)

csv에는 아래와 같이 정보가 잘 담겨있었다.

<STEP5> Q-learning 

사용할 오픈 소스의 입력값에 우리의 정보를 입력하고 알고싶은 start 점과 end점을 입력하여 최단경로를 탐색해주었다.

아래의 코드는 수정한 메인 코드이고 import해주는 함수는 모두 오픈소스 그대로 사용했기 때문에 따로 포스팅하지 않겠다.

from get_dict import get_dict
from get_R_Q import initial_R
from get_R_Q import initial_Q
from get_result import get_result

import pandas as pd
import time

data = pd.read_csv("/content/gdrive/MyDrive/AI-SW-HACK/graph.csv")
graph = get_dict(data)

A = graph["A"]
Z = graph["Z"]
weight = graph["weight"]
A_Z_dict = graph["A_Z_dict"]

##
start = 40
end = [51]

R = initial_R(A,Z,weight,A_Z_dict)
Q = initial_Q(R)

alpha = 0.7 # learning rate
epsilon = 0.1 #greedy policy
n_episodes = 1000

time0 = time.time()
result = get_result(R,Q,alpha,epsilon,n_episodes,start,end)
print("time is:",time.time() - time0)

print(result["ends_find"])
print(result["cost"])
print(result["routes_number"])

print(result["all_routes"])

 

<STEP6> 최적경로 보여주기

위에서 구한 최적경로를 시작점으로부터 끝점까지 선을 하나씩 그려가며 저장한 이미지를 이어붙여 하나의 video로 만들어 보여주었다.

코드는 다음과 같다.

Final_routes = result["all_routes"][end[0]][0]
print(Final_routes)

SHOW = Look.copy()

path = '/content/gdrive/MyDrive/AI-SW-HACK/Show_image/show_image0.jpg'
cv2.imwrite(path,SHOW)

for i in range(0,len(Final_routes)-1):
  num1 = Final_routes[i]
  num2 = Final_routes[i+1]
  SHOW = cv2.line(SHOW, (all_circle[num1][0],all_circle[num1][1]), (all_circle[num2][0],all_circle[num2][1]), (255,255,255), 8, 8, 0)
  path = '/content/gdrive/MyDrive/AI-SW-HACK/Show_image/show_image' + str(i+1) + '.jpg'
  cv2.imwrite(path,SHOW)
  #cv_imshow(SHOW)

cv_imshow(SHOW)

import glob

img_array = []
for filename in glob.glob('/content/gdrive/MyDrive/AI-SW-HACK/Show_image/*.jpg'):
    img = cv2.imread(filename)
    height, width, layers = img.shape
    size = (width,height)
    img_array.append(img)
 
 
out = cv2.VideoWriter('/content/gdrive/MyDrive/AI-SW-HACK/Show_video/show_video.avi',cv2.VideoWriter_fourcc(*'DIVX'), 1, size)
 
for i in range(len(img_array)):
    out.write(img_array[i])
out.release()

 

이렇게 나온 결과물 video가 성공적이었다. 

show_video.avi
0.26MB

 

구현은 이렇게 성공적으로 끝이 났으며, 이미지를 더 예쁘게 꾸며 사용자가 보기 편하게 만들었다.

 

 

여기까지가 내가 담당하여 작성했던 코드이다.

 

# 후기 

 밤새는 과정이 힘들었고, 다른 프로젝트에 비해 기한이 짧았기에 촉박한 마음이 들었었다.

 하지만 그만큼 몰입하여서 프로젝트를 진행할 수 있었다.

 힘든 과정 속에 서로에게 힘이 되어준 팀원들이 없었다면 이렇게 성공적으로 프로젝트를 끝낼 수 없었을 것이다.

 적극적으로 문제를 해결하려고 노력하고, 소통을 잘하는 팀원들과 함께여서 너무 다행이었다.

 항상 느끼는 바이지만 모든 걸 혼자할 수는 없고, 여럿이 의견을 모으고 힘을 모았을 때 더 좋은 결과가 나오는 것 같다.

 다양한 시각으로 접근해 보고 새로운 지식을 쌓으며 함께 성장해가는 값진 경험이 되었다.

 이번 프로젝트에서 한가지 아쉬운 점이 있다면 앱 개발을 담당했던 친구들이 예쁘게 앱을 만들었으나,

 비디오 데이터를 colab에서 앱으로 연동시키는 방법을 찾지 못해 앱을 사용해보지 못했다는 점이다.

 colab이 아닌 다른 환경에서 인공지능 코드를 구현하고 백엔드에 대한 지식이 충분히 있었다면,

 앱 연동까지 가능하였을 텐데 시간이 부족하여 이를 공부하고 실현할 수 없었다.

 좋은 AI를 만드는 것도 중요하지만, 이 모델을 사용할 수 있게 연동시키는 법도 배워야할 것 같다.

  다음에는 꼭 연동시켜 실제로 상용화 가능한 결과물을 내보자!