본문 바로가기
카테고리 없음

자율 주행 로봇 - 원격 모니터링

by zhsus 2023. 2. 24.
반응형

📒 작품 소개  ☆

 

학교 제어 공학 연구실에서 들어가게 되어 자율 주행 로봇 제작 프로젝트에 참여하였다.

 

학과 층에서 목적지를 입력하면 장애물을 피해 다니면서 목적지까지 무사히 도착할 수 있게 하는 로봇을 만드는 프로젝트이다.

 

프로젝트 중간에 참여하게 되어 원격으로 모니터링할 수 있는 어플리케이션을 제작하는 역할을 맡게 되었다.

 

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. 목표

  1. 자율 주행 로봇이 목적지까지 장애물을 회피하며 도착할 수 있도록 한다.
  2. 목적지에 도착했을 경우, 사용자의 인식을 통해 목적지까지 도착했음을 확인한다.
  3. 동시에, 원격에서 사용자와 관리자가 실시간으로 확인할 수 있으며, 긴급한 상황에 관리자가 원격에서 제어할 수 있는 어플리케이션을 제작한다.

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' /> &nbsp;&nbsp;
          <input type="submit" value="front" name="f" /> &nbsp;&nbsp;
          <input type="submit" value="left" name="l" /> &nbsp;&nbsp;
          <input type="submit" value="right" name="r" /> &nbsp;&nbsp;
          <input type="submit" value="back" name="b" /> &nbsp;&nbsp;
        </form>

5. 활동 결과

  • 영상 스트리밍 및 사용자 인식 구현
  • 결과↓

스트리밍과 사용자 인식 동작 결과 이미지

  • 원격 제어 버튼 입력에 따른 이동 속도 그래프
  • 결과↓

원격 제어 버튼 입력에 따른 이동 속도 그래프

  • 아쉬웠던 점 : 라이더 센서에서 읽어오는 값들을 웹 브라우저에서 보여주기 위해 데이터 변환이 필요한 것 같아보였는데, 방법을 찾지 못해서 프로젝트 완성을 끝마치지 못한게 많이 아쉬웠다.

6. 마무리

전반적으로 다 처음 다뤄보는 파트였다보니까, 많이 어설프게 완성한 부분이 많다.

 

Python의 Flask를 활용해서 웹 애플리케이션 제작을 해보면서, 전체적인 웹 동작 구성에 대해 배울 게 있었다.

 

웹 페이지에서 동작 명령을 주면, 로봇이 명령을 받아서 동작을 수행할 수 있는 것을 확인해볼 수 있었다.

그리고 웹 페이지에서 실시간으로 반응하는 그래프를 적용해보면서 웹 제작에 대한 큰 틀을 머리속에 넣을 수 있었다.

 

아쉽게 완성하지 못한 기능적인 부분이 있었지만, 더 공부해서 다음 기회에 한번 더 도전할 것이다.🔥🔥

반응형