목표 | 컴퓨터 비전을 사용해서 자율 주행 기부 로봇 만들기 |
기간 | 2019.09-2019.12 |
역할 | 소프트웨어 개발 담당 |
언어/환경 | Python3/Ubuntu 18.04(Tx2 board, logitech depth camera) |
라이브러리 | torch, numpy, cv2, preprocess, argparse, pickle |
1. 계기
자율 주행 로봇 개발은 [지능형 로봇 크래쉬 랩]이라는 로봇 공학과의 PBL 수업이었다. 이미 졸업에 필요한 전공 학점을 다 채운 나에게 필요한 과목이 아니었다. 즉 단순히 흥미를 위해 신청한 과목이었다(그리고 재밌을 것 같았다).
# 세상은_내가_원하는대로만_굴러가지_않는다
원래 이 과목은 소프트웨어학부 학생 2명, 로봇 공학과 학생 3명으로 팀이 이루어지는 과목이었다.
그런데 어쩌다 보니 우리 팀은 소프트웨어학부 학생 1명(나🙄!), 로봇 공학과 학생 3명으로 이루어진 팀이 되었다. 다른 팀은 적어도 소프트웨어학부 학생이 2명은 있거나 3명인 팀이 있었는데! 우리 팀만...(그래도 우리 팀이 최고였다)
2. 역할 & 과정
처음에 우리는 어떤 로봇을 개발할지 고민했다. 여러 아이디어가 나왔지만... 역시 로봇 하면 자율 주행 로봇 아닌가! 우리는(특히 나는...) 앞으로 다가올 시련을 알지 못한 채 자율 주행 로봇을 만들기로 했다.
9월 | 이론 공부(ROS, Computer Vision) | 모두 잘잠 |
10월 | 하드웨어 개발 & 소프트웨어 개발 | 팀원들이 잠을 못잠 |
11월 | 소프트웨어 개발 & 하드웨어 개발 | 내가 못잠 |
12월 | 최종 테스트 → 수정 → 최종_최종_테스트 → 진짜_최종_최종_테스트 → ... | 모두 못잠 |
9월에는 앞으로의 계획이나 이론 공부를 해서 모두 행복했다. 로봇 개발을 위해 주문한 재료들을 기다려야 했기에 모두 평화로웠다.
그러나 10월.. 재료들이 차례차례 도착하고 팀원들은 잠을 자지 못했다.. 고 들었다! 물론 나는 잘 잤다.
물론 하드웨어 개발과 어느 정도 속도를 맞추기 위해서 노트북에서(Mac) 얼굴/사람 앞, 뒷모습 인식이나, 좌표 출력 간격 같은 자잘한 기능을 개발했다.
11월-12월은 나에게 지옥이었다. 팀원들은 센서를 보다 정밀하게 동작하도록 하는 것을 테스트하고 있었고, 나는 Tx2보드에서 본격적으로 개발을 시작했다.
# 다중 보행자 라벨링 구현
나는 이때 MacOS에서 개발한 것을 뼈저리게 후회했다. TX2보드에서 개발은 못했더라도 최소한 Ubuntu에서는 개발을 해야 했다... 그리고 ROS 개발을 하는 분과 좀 더 꼼꼼히 어떻게 진행되고 있는지 공유를 해야 했다.
TX2, Ubuntu 18.04, Python3에서 내가 기존에 보행자 라벨링을 하기 위해 사용했던 트랙커가 동작하지 않았고, Python2로는 트랙커가 동작하긴 했지만... 코드 전체를 갈아엎어야 했다.
그래서 Ubuntu 16.04로 바꿔볼까도 생각했지만, ROS를 개발하는 팀원분이 호환성 문제로 반대하셨고(격렬한 반대는 아니었지만, 정말 힘들어 보이셔서 알겠다고 했다) 결국 보행자 라벨링은 내가 알아서 잘 해결해보기로 했다.
당시 다른 팀의 소프트웨어 개발 담당들에게 물어보니까 다들 Ubuntu 버전을 16.04로 낮추거나 코드 전체를 Python2로 바꾸는 노가다를 했었다😣.
난 도저히 그렇게까지 하고 싶지 않았기에, 직접 트랙킹 기능을 하는 함수를 만들었다. 트랙킹 알고리즘에 대한 설명은 인터넷 여기저기에 많이 나와있었고, 다중 보행자 라벨링이 조금 까다롭긴 했지만.. 알고리즘을 구현해서 테스트를 할 때마다 카메라 앞에서 왔다 갔다 해주는 팀원들 덕에 재밌어서 괜찮았다.
# 추적 대상 선정 # 추적 대상 좌표 출력
다중 보행자 라벨링을 마치고, 우리는 단 한 명의 보행자만 추적해야 했기 때문에 목표로 할 대상을 선택해야 했다.
곰돌이 로봇의 주행 방식은
1. 타깃 선정(거리/ 로봇 인지 여부 파악)
2. 타깃에게 이동
이 끝이다.
보행자가 걸어다는 것을 보며 가장 적합한 대상자를 고르고(나는 보행자 중 곰돌이 로봇에게 관심을 가지고 있다고 생각할 수 있는, 일정 시간 이상 곰돌이 로봇을 쳐다보는 경우를 추적 대상으로 선정하게 했다) 타깃에게 다가간다.
타깃이 뒤돌아도 같은 인물 여부를 판단할 수 있도록 하기 위해 트랙커를 사용하려고 했는데, 사용이 안돼서(스택오버플로우에 검색해보니까 Tx2보드에서 사용이 안된다고 많은 글이 올라와있었다) 직접 구현했고,
타깃이 일정 속도 이상으로 멀어져 가면(로봇이 달릴 수 있는 속도 이상으로) 포기하고 다른 대상자를 찾도록 했다.
코드 일부🔽
from __future__ import division
import time
import torch
import torch.nn as nn
from torch.autograd import Variable
import numpy as np
import cv2
from util import *
from darknet import Darknet
import random
import argparse
import pickle as pkl
from matplotlib import pyplot as plt
from collections import Counter
from utils import non_max_suppression
import datetime
WINDOW_NAME = "TestYOLO"
def get_test_input(input_dim, CUDA):
img = cv2.imread("imgs/messi.jpg")
img = cv2.resize(img, (input_dim, input_dim))
new_img = img[:, :, ::-1].transpose((2, 0, 1))
new_img = new_img[np.newaxis, :, :, :] / 255.0
new_img = torch.from_numpy(new_img).float()
new_img = Variable(new_img)
if CUDA:
new_img = new_img.cuda()
return new_img
def prep_image(img, inp_dim):
original = img
dim = original.shape[1], original.shape[0]
img = cv2.resize(original, (inp_dim, inp_dim))
new_img = img[:, :, ::-1].transpose((2, 0, 1)).copy()
new_img = torch.from_numpy(new_img).float().div(255.0).unsqueeze(0)
return new_img, original, dim
def write(x, img):
global count_people, box_center
flag = 0
c1 = tuple(x[1:3].int())
c2 = tuple(x[3:5].int())
name = int(x[-1])
counting_label = "{0}: {1}".format(classes[name], str(counter[name]))
if classes[name] == "person":
color = colors[1]
b1 = c1[0].cpu().numpy()
b2 = c2[0].cpu().numpy()
center = int((b1 + b2) / 2)
n = len(box_center) - 1
if 20 < center < 120:
if len(box_center) > 1:
if abs(box_center[n - 1] - box_center[n]) < 10:
box_center.append(center)
cv2.rectangle(img, c1, c2, color, 1)
if len(box_center) > 10:
flag = 1
elif len(box_center) < 2:
box_center.append(center)
flag = 0
if len(box_center) > 1 and abs(box_center[n] - box_center[n - 1]) > 10:
box_center.clear()
count_people += 1
if flag == 1:
t_size = cv2.getTextSize(counting_label, cv2.FONT_HERSHEY_PLAIN, 1, 1)[0]
c2 = c1[0] + t_size[0] + 3, c1[1] + t_size[1] + 4
cv2.rectangle(img, c1, c2, color, -1)
cv2.putText(img, str(count_people), (c1[0], c1[1] + t_size[1] + 4), cv2.FONT_HERSHEY_PLAIN, 1,
[255, 255, 255], 1)
return img
def arg_parse():
parser = argparse.ArgumentParser(description='YOLO v3 Cam Demo')
parser.add_argument("--confidence", dest="confidence", help="Object Confidence to filter predictions", default=0.25)
parser.add_argument("--nms_thresh", dest="nms_thresh", help="NMS Threshhold", default=0.4)
parser.add_argument("--reso", dest='reso', help= "Input resolution of the network. Increase to increase accuracy. Decrease to increase speed", default="160", type=str)
return parser.parse_args()
def open_window(width, height):
cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_NORMAL)
cv2.resizeWindow(WINDOW_NAME, width, height)
cv2.moveWindow(WINDOW_NAME, 800, 800)
if __name__ == '__main__':
count_people = 0
box_center = []
cfg_file = "cfg/yolov3.cfg"
weights_file = "yolov3.weights"
num_classes = 80
args = arg_parse()
confidence = float(args.confidence)
nms_thresh = float(args.nms_thresh)
start = 0
CUDA = torch.cuda.is_available()
bbox_attrs = 5 + num_classes
print("Model loading....")
model = Darknet(cfg_file)
model.load_weights(weights_file)
model.net_info["height"] = args.reso
inp_dim = int(model.net_info["height"])
assert inp_dim % 32 == 0
assert inp_dim > 32
if CUDA:
model.cuda()
model.eval()
counter = {}
video_file = 'video.avi'
print("Window open")
open_window(160, 160) # 500,500
gst_str = ('nvarguscamerasrc sensor-id=0 ! '
'video/x-raw(memory:NVMM), '
'width=(int)512, height=(int)512, '
'format=(string)NV12, framerate=(fraction)15/1 ! '
'nvvidconv ! '
'video/x-raw, width=(int){}, height=(int){}, '
'format=(string)BGRx ! '
'videoconvert ! appsink').format(args.reso, args.reso)
print("capture video")
cap = cv2.VideoCapture(1) # gst_str
cap.set(3, 160)
cap.set(4, 160)
assert cap.isOpened(), 'Cannot capture source'
frames = 0
num_person = 0
start = time.time()
print("start reading..")
plt.ion()
while cap.isOpened():
ret, frame = cap.read()
if ret:
img, orig_im, dim = prep_image(frame, inp_dim)
im_dim = torch.FloatTensor(dim).repeat(1, 2)
if CUDA:
im_dim = im_dim.cuda()
img = img.cuda()
with torch.no_grad():
output = model(Variable(img), CUDA)
output = non_max_suppression(output, 0.5, num_classes, nms_conf=0.4)
if type(output) == int:
frames += 1
print("FPS of the video is {:5.2f}".format(frames / (time.time() - start)))
cv2.imshow(WINDOW_NAME, orig_im)
key = cv2.waitKey(1)
if key & 0xFF == ord('q'):
break
continue
counter = Counter(output[:, -1].cpu().numpy().astype(int).tolist())
output[:, 1:5] = torch.clamp(output[:, 1:5], 0.0, float(inp_dim)) / inp_dim
im_dim = im_dim.repeat(output.size(0), 1)
output[:, [1, 3]] *= frame.shape[1]
output[:, [2, 4]] *= frame.shape[0]
classes = load_classes('data/coco.names')
colors = pkl.load(open("pallete", "rb"))
count_people = 0
cv2.imshow("frame", orig_im)
key = cv2.waitKey(1)
if key & 0xFF == ord('q'):
break
frames += 1
print("FPS of the video is {:5.2f}".format(frames / (time.time() - start)))
else:
break
저 코드를 작성할 때.. 나는 정신이 나간 상태였다. 팀원은 옆에서 3D 펜으로 하트 팔찌와 반지를 만들어줬다. 프러포즈인 줄😮
# 뎁스 카메라 포기 # 카메라는 하나만 쓰자
처음에는 뎁스 카메라를 사용해서 추적 대상과의 거리를 추측했는데 우리 팀의 로봇은 자율주행 로봇이었고.. 여러 가지 별도로 처리해야 하는 게 많았다. 사람 말고도 장애물도 인식해야 하고.. 와중에 센서 데이터도 처리해야 하고..
그래서 이 부분은 그냥 단일 카메라 사용하고 거리 추측은 내가 잘 테스트해서 해보기로 했다(결국 이거 테스트하느라 12월 내내 거의 못 잤다).
3. 결과물
위에서 언급한 것 외에도 챗봇을 만들어서 로봇을 홍보하고, GUI로 인터페이스를 만들어서 크리스마스 느낌이 나게 노래도 나오도록 했다.
여러모로 바빴지만.. 정말 재밌었다. 그리고 우리 팀은 데모를 할 때 유일하게 소프트웨어에 문제가 안 생긴 팀이었다 ㅎㅎ 구현하지 못할 것 같은 기능은 빼고, 기간 내에 할 수 있는 것에만 집중한 덕이었다고 생각한다.