📒 작품 소개 ☆
학교 제어 공학 연구실에서 들어가게 되어 자율 주행 로봇 제작 프로젝트에 참여하였다.
학과 층에서 목적지를 입력하면 장애물을 피해 다니면서 목적지까지 무사히 도착할 수 있게 하는 로봇을 만드는 프로젝트이다.
프로젝트 중간에 참여하게 되어 원격으로 모니터링할 수 있는 어플리케이션을 제작하는 역할을 맡게 되었다.
https://github.com/hwanggiju/autodriving_robot.git
GitHub - hwanggiju/autodriving_robot: 자율주행로봇 모니터링
자율주행로봇 모니터링. Contribute to hwanggiju/autodriving_robot development by creating an account on GitHub.
github.com
목차
1. 개발환경
2. 목표
3. 나의 역할
4. 활동 내용
5. 활동 결과
6. 마무리
#4 자율 주행 로봇 - 원격 모니터링
작품 기간 : 2021.03 ~ 2022.08 (약 1년 5개월)
1. 개발 환경
- 개발 언어 : Python, Java Script
- OS : Ubuntu 20.04LTS
- 개발 도구 : VisualStudio Code, Github, Putty
- 개발 보드 : Raspberry Pi 4. Arduino mega
2. 목표
- 자율 주행 로봇이 목적지까지 장애물을 회피하며 도착할 수 있도록 한다.
- 목적지에 도착했을 경우, 사용자의 인식을 통해 목적지까지 도착했음을 확인한다.
- 동시에, 원격에서 사용자와 관리자가 실시간으로 확인할 수 있으며, 긴급한 상황에 관리자가 원격에서 제어할 수 있는 어플리케이션을 제작한다.
3. 나의 역할
- 웹 어플리케이션 제작
- 이동 경로 실시간 영상 스트리밍 구현
- 목적지 도착을 확인하기 위한 사용자 인식 코드 구현
- 로봇 이동 속도 실시간 모니터링 구현
- 로봇 원격 제어
- 라이더 센서로 센싱된 값을 어플리케이션 지도 생성 (미완성...)
4. 활동 내용
>> 웹 어플리케이션 제작
- 메인 브라우저에서는 카메라로 로봇이 이동하는 영상을 볼 수 있고, 버튼을 통해 사용자를 인식할 수 있는 기능이 있다.
- 두번째 브라우저에서는 로봇의 이동 속도를 모니터링하는 그래프를 볼 수 있고, 버튼 입력을 통해 로봇 이동을 강제로 제어할 수 있다.
- 마지막 브라우저는 라이더의 센싱된 값을 지도로 나타내주고, 사용자가 목적지를 입력할 수 있는 페이지로 설계하였다.
- 웹 제작 구상은 다음과 같이 설계해보았다.
- 웹 서버의 경우, Python의 Flask 라이브러리를 활용하여 제작하였다.
- 먼저, 아래 명령어로 Flask 라이브러리를 pip을 통해 설치를 먼저 받아준다.
pip install flask
- 다음 flask 라이브러리 도구들을 임포트해준 후, app 변수로 어플리케이션 선언을 통해 각 기능들을 연결시켜주는 역할을 할 수 있다.
- 웹 서버를 실행하는 코드가 되겠다.
from flask import Flask, Response, render_template, request, make_response
# 어플리케이션 선언
app = Flask(__name__, static_url_path='/static')
if __name__ == '__main__':
# 웹 서버 실행
app.run(host='0.0.0.0', threaded=True, debug=True)
>> 이동 경로 실시간 영상 스트리밍 구현
- 로봇의 카메라 장치를 읽어온 후, 읽어온 프레임을 이미지로 인코딩해주고 웹에서 보여주기위해 HTTP 형식에 맞게 개행문자를 추가하여 반환시켜준다.
- user 변수의 경우, 이후 객체 인식 코드 구현 부분에서 사용되는 변수로써 이후에 더 자세하게 설명할 것이다.
import cv2
# 카메라 device 연결
cap = cv2.VideoCapture(0)
# 카메라 frame 읽어오고 웹에서 표현하는 형식으로 인코딩해주는 함수
def gen_frame(cap):
# 웹 브라우저에서 객체 인식하라는 버튼 입력을 받으면,
# 객체 인식 함수를 실행하기 위한 신호를 알려주기 위한 변수
global user
while True:
success, frame = cap.read() # 카메라 읽기
if success:
# 스트리밍과 같이 동작하면, 스트리밍 속도 저하 문제
# 객체 인식이 필요한 상황에만 적용할 수 있도록 작업하였음.
if (user) :
user = 0
name = user_detect(frame) # 객체 인식 함수
try :
ret, jpeg = cv2.imencode('.jpg', cv2.flip(frame, 1)) # 프레임 -> 메모리 버퍼로 인코딩
frame = jpeg.tobytes()
# HTTP 응답으로 전송하는데 필요한 형식으로 전환
yield (b'--frame\r\n'b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n\r\n')
except Exception as e:
pass
else :
pass
- 위에서 한 장의 프레임을 이미지로 바꾸어 웹으로 전송하기 위한 데이터 작업을 마쳤다면, app으로 '/stream' 이라는 url로 해당 기능 함수를 연결시켜준다.
- response로 이미지를 html 페이지로 보내주는 역할을 하게된다.
# 카메라 실시간 스트리밍 화면 보여주는 함수
@app.route('/stream')
def stream():
global cap
return Response(gen_frame(cap), \
mimetype='multipart/x-mixed-replace; boundary=frame')
- html 페이지에서는 위 이미지를 아래와 같이 응답을 받을 수 있다.
<!-- 영상 띄우기 -->
<img src="{{ url_for('stream') }}" height="30%" width="30%">
>> 목적지 도착을 확인하기 위한 객체 인식 코드 구현
- dlib을 이용하여 얼굴 인식 기능 구현을 하였다. 테스트 인물로 오바마와 바이든, 나의 얼굴 이미지를 등록하고 얼굴에 대한 데이터를 리스트에 저장한 후에 얼굴 이미지 리스트와 매칭되는 이름 리스트를 만들어주었다.
# 이미지 파일 dlib 활용한 face 로드
obama_image = face_recognition.load_image_file("image_dir/obama.jpg") # 이미지 로드
obama_face_encoding = face_recognition.face_encodings(obama_image)[0] # 이미지 인코딩 후 데이터 저장
biden_image = face_recognition.load_image_file("image_dir/biden.jpg") # 이미지 로드
biden_face_encoding = face_recognition.face_encodings(biden_image)[0] # 이미지 인코딩 후 데이터 저장
giju_image = face_recognition.load_image_file("image_dir/giju_image.jpg") # 이미지 로드
giju_face_encoding = face_recognition.face_encodings(giju_image)[0] # 이미지 인코딩 후 데이터 저장
# 이미지 인코딩 데이터 리스트 저장
known_face_encodings = [
obama_face_encoding,
biden_face_encoding,
giju_face_encoding
]
# 인코딩 데이터 리스트와 매칭되는 고유값 리스트
known_face_names = [
"Barack Obama",
"Joe Biden",
"Gi ju"
]
- 다음으로 인식 버튼을 입력받을 때, 카메라에서 읽어온 프레임에서 찾은 얼굴 이미지 데이터와 위에서 저장한 얼굴 데이터와 비교하여 다음 얼굴 중 가장 비슷한 인물 얼굴의 이름을 반환해주는 코드이다.
# 사용자 인식 구현 함수
def user_detect(frame) :
global name
rgb_frame = frame[:, :, ::-1]
face_locations = face_recognition.face_locations(rgb_frame) # 찾은 얼굴 값
face_encodings = face_recognition.face_encodings(rgb_frame, face_locations) # 얼굴 인코딩
for face_encoding in face_encodings:
matches = face_recognition.compare_faces(known_face_encodings, face_encoding)
face_distances = face_recognition.face_distance(known_face_encodings, face_encoding)
best_match_index = np.argmin(face_distances)
if matches[best_match_index]:
name = known_face_names[best_match_index]
return name
return name
- 위 gen_frame() 함수에서 user 변수를 선언한 이유는 객체 인식 기능을 활성화/비활성화 시켜주는 변수로 만들어준 것이다.
- 문제점 : 이 기능을 on/off로 만들어준 이유는 스트리밍과 동시에 진행하게 될 경우, 스트리밍 영상이 크게 느려지게 되서 모니터링 기능에 영향을 주기 때문이었다.
- 해결 방법 : 로봇이 이동중에는 비활성화 상태였다가, 목적지에 도착했을 경우 활성화되어 도착 확인을 할 수 있도록 구현하였다.
- 웹 브라우저에서 POST 메서드를 통해 사용자 얼굴 인식 버튼과 스트리밍 동작 실행/정지 버튼을 입력할 수 있다.
User 버튼을 입력할 경우, 사용자 인식 기능을 활성화할 수 있고
Stop/Start 버튼을 입력할 경우, 스트리밍을 켜고 끌 수 있다. - 마찬가지로 app으로 '/requests' url과 tasks() 함수를 연결시켜주었다.
- 반환하는 값은 user_detect() 함수에서 반환하는 이름을 html 페이지로 보내주게 된다.
# 웹 브라우저 신호 입력으로 받아서 처리되는 부분
@app.route('/requests', methods=['POST', 'GET'])
def tasks() :
global switch, name, cap
# POST 메서드를 사용하여 웹 브라우저의 신호를 받음
if request.method == 'POST' :
# 객체 인식 버튼 입력 시
if request.form.get('clicked') == 'User':
global user
user = 1
# 스트리밍 ON/OFF 버튼 입력 시
elif request.form.get('camera_stop') == 'Stop/Start':
if(switch==1):
switch = 0
cap.release()
else :
cap = cv2.VideoCapture(0)
switch = 1
# GET 메서드를 사용할 가능성으로 작성만 해놓은 것.
# 실제로 사용되는 곳은 아직 없음.
elif request.method == 'GET' :
return render_template('main.html')
return render_template('main.html', value = name)
- html 페이지에서 tasks() 함수로 post 메서드 입력 신호를 전송하는 버튼을 생성해준다. 그 다음 tasks()에서 전송한 사용자 이름을 반환 받아 출력해줄 수 있도록하였다.
<!-- 백엔드에 입력을 주는 버튼 -->
<form method='post', action="{{url_for('tasks')}}">
<input type="submit" value="Stop/Start" name="camera_stop" />
<input type="submit" value="User" name="clicked" /></br>
</form>
...
<div class='print'> 사용자 {{value}} 확인했습니다. </div>
>> 로봇 이동 속도 실시간 모니터링 구현
- 모터 동작은 아두이노 보드로 만들었다. 라즈베리파이와는 시리얼 통신을 통해 데이터를 주고 받았으며, port 번호와 통신 속도를 아두이노에서 설정한 것도 동일하게 설정해주었다.
- 그리고, 아두이노의 시리얼 데이터를 읽어오기 위해 Python에서 serial 도구를 활용하였고, 시리얼 데이터를 읽어온 데이터는 문자열로 저장되기 때문에, 실수형으로 자료형을 변환해주었다.
- 마지막으로 읽어온 데이터를 Java Script 파일로 전송해주게 된다.
- 여기서는 app으로 '/live-data'의 url과 live_data() 함수를 연결시켜주었다.
import serial
# 아두이노 시리얼 포트 넘버와 brate 설정
port = '/dev/ttyACM0'
brate = 115200
# 데이터 그래프 실시간으로 그려주는 동적 함수 - 자바 코드와 연동됨
@app.route('/live-data')
def live_data():
# 아두이노 Serial 데이터 읽어오기 위한 설정값
ser = serial.Serial(port, brate, timeout=None)
# Serial 데이터 읽어오기
SerialData = ser.readline()
# print(SerialData)
SerialData = SerialData.decode(errors='ignore')[:4] # 읽어온 데이터 슬라이싱
# 자바 함수의 입력으로 반환
response = make_response(json.dumps(float(SerialData)))
response.content_type = 'application/json'
return response
- Java Script 파일에서는 실시간으로 받아온 데이터를 그래프로 그려주는 코드로 구현하였다.
- 아래 코드는 requestData() 함수를 통해 '/live-data' 과 연결되고 live_data() 함수에서 보내주는 시리얼 데이터를 1초마다 받아와서 그래프를 그려주는 코드이다.
- 또한, javascript 코드를 html과 연동하기 위해 'data-container'라는 이름으로 매칭하고, 그래프 축과 제목 등을 지정하는 코드가 포함되어있다.
var chart;
// 데이터 응답
function requestData() {
$.ajax({
// 웹페이지와 연결되는 url
url: '/live-data',
// 데이터가 들어오면 실행되는 함수
success: function(point) {
var series = chart.series[0],
// 한 화면 그래프에 그려지는 데이터 최대 개수 20개 설정
shift = series.data.length > 20;
// 다음 포인트 지정
chart.series[0].addPoint(point, true, shift);
// delay 1초
setTimeout(requestData, 1000);
},
cache: false
});
}
// css 기반 그래프 x, y, 제목 등 기본 지정
$(document).ready(function() {
chart = new Highcharts.Chart({
chart: {
renderTo: 'data-container',
defaultSeriesType: 'line',
events: {
load: requestData
}
},
title: {
text: 'Live Motor data'
},
xAxis: {
type: 'datetime',
tickPixelInterval: 150,
maxZoom: 20 * 1000
},
yAxis: {
minPadding: 0.2,
maxPadding: 0.2,
title: {
text: 'Value',
margin: 80
}
},
series: [{
name: 'Motor data',
data: []
}]
});
});
- 여기까지 그래프를 그릴 수 있는 조건을 갖추었고, html 페이지에서 보여주는 코드는 아래와 같다.
- 위의 javascript 파일을 저장된 경로로부터 불러오고, 'data-container'라는 id 이름으로 javascript 그래프 정보를 가져와서 html 페이지로 그려주게 된다.
<!-- 그래프 그리기 -->
<div class="container-fluid" id="data-container"></div>
<!-- highcharts.js 파일 저장 경로 지정 -->
<script src="저장 경로/highcharts.js"></script>
>> 로봇 원격 제어
- 모터 속도 그래프가 그려지는 웹 브라우저와 함께 구현하였다.
- 여기서도 POST 메서드의 입력을 받아 버튼으로 로봇 동작을 구현하였다.
- 이번엔 아두이노 시리얼 통신으로 값을 전송해서 신호를 전달한다. 버튼은 정지와 전진, 좌회전, 우회전, 후진 동작 명령어를 넣어주었다.
- 여기서는 app으로 '/requests1'이라는 url로 gostop() 함수와 연결시켜주었다.
# 모터 동작 명령 수행
# stop, front, left, right, back, reset, Pos 명령어
@app.route('/requests1', methods=['POST'])
def gostop() :
ser = serial.Serial(port, brate)
direction = ''
if request.method == 'POST' :
if request.form.get('s') == 'stop' :
direction = 'STOP'
ser.write('stop'.encode())
if request.form.get('f') == 'front' :
direction = '전진'
ser.write('front'.encode())
if request.form.get('l') == 'left' :
direction = '좌회전'
ser.write('left'.encode())
if request.form.get('r') == 'right' :
direction = '우회전'
ser.write('right'.encode())
if request.form.get('b') == 'back' :
direction = '후진'
ser.write('back'.encode())
return render_template('index.html', value=direction, encode='utf-8')
- 모터 동작 코드에서는 각각의 명령어를 통해 입력 받았을 때, 동작할 수 있는 코드로 팀원이 구현해주었다.
- html 페이지에서는 gostop() 함수로 post 메서드로 입력 신호를 전달하는 버튼을 생성하고, 동작 상태값을 응답받아서 출력해주었다.
<!-- 제어 명령 버튼 입력 -->
<div class="container-fluid">
<div class="row">
<form method = 'POST', action="{{url_for('gostop')}}">
<div class='print'> 진행 상태 : {{value}} </div>
<input type="submit" value="stop" name='s' />
<input type="submit" value="front" name="f" />
<input type="submit" value="left" name="l" />
<input type="submit" value="right" name="r" />
<input type="submit" value="back" name="b" />
</form>
5. 활동 결과
- 영상 스트리밍 및 사용자 인식 구현
- 결과↓
- 원격 제어 버튼 입력에 따른 이동 속도 그래프
- 결과↓
- 아쉬웠던 점 : 라이더 센서에서 읽어오는 값들을 웹 브라우저에서 보여주기 위해 데이터 변환이 필요한 것 같아보였는데, 방법을 찾지 못해서 프로젝트 완성을 끝마치지 못한게 많이 아쉬웠다.
6. 마무리
전반적으로 다 처음 다뤄보는 파트였다보니까, 많이 어설프게 완성한 부분이 많다.
Python의 Flask를 활용해서 웹 애플리케이션 제작을 해보면서, 전체적인 웹 동작 구성에 대해 배울 게 있었다.
웹 페이지에서 동작 명령을 주면, 로봇이 명령을 받아서 동작을 수행할 수 있는 것을 확인해볼 수 있었다.
그리고 웹 페이지에서 실시간으로 반응하는 그래프를 적용해보면서 웹 제작에 대한 큰 틀을 머리속에 넣을 수 있었다.
아쉽게 완성하지 못한 기능적인 부분이 있었지만, 더 공부해서 다음 기회에 한번 더 도전할 것이다.🔥🔥