라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

안녕하세요 cocoon입니다. 요즘 코로나19로 인해 분위기도 안 좋고 대학교도 한 학기 전부 사이버강의 진행해서 말이 많은 것 같습니다. 맨날 집에만 있다 보니 답답하기도 하고(집돌이라 그래도 심심하지는 않은 것 같아요.. ㅎㅎ) 자꾸 나태해지는 거 같아요 ㅜㅜ 원하는데 취직하려면 그래도 열심히 해야죠^^ ㅎㅎ....  

아무튼 올해 기계공학과 4학년이 되면서 졸업논문도 준비해야하는데 논문 주제를 최적 설계나, CDF, 유한요소 설계 등등 과에 관련된 거보다 아무래도 진로를 프로그래머 쪽으로 잡았기 때문에 평소에 관심 있었고 대학 생활하면서 꼭 제작해보고 싶었던 작품을 만들어 보는 게 낫겠다고 생각했습니다. 그래서 마침 2~3학년 때 스마트 디자인 캠프나 해커톤 하면서  개발 보드랑 센서를 많이 모와놔서 부품들을 활용해서 자율주행 자동차를 만들어 보려고 합니다.

물론 아두이노와 전방에 초음파 달고 모터를 달기만 한 작품은 너무 간단한것같아서 카메라 모듈을 이용해 영상 처리하여 차선 유지, 곡선주행을 구현해보려 합니다. 작년에 친구가 졸업작품 만든다고 라즈베리파이로 카메라 영상을 웹 스트리밍 하는 것을 도와줬었는데 많이 어려웠었습니다. OpenCV를 활용하는 것은 아무래도 처음 하는데 많이 어려울 것이라고 예상됩니다.

라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

1주차, 2주 차는 주제 선정하고 PPT 만들어서 팀즈(화상회의 프로그램)로 발표를 했네요. 이번 주가 3주 차인데 4월 1일 수요일에 용산 전자랜드에 다녀와서 필요한 부품들(바퀴, 조향 부품, 모터, 모터 드라이버, 초음파 센서, 다이오드 등등)을 구매하였습니다. 검정 종이와 하드보드지는 시험 주행할 트랙을 만드려고 합니다.

라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng
라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

제작 목표중에서 가장 중점적으로 둘 것은 차선 유지, 표지판, 신호등 인식일 것 같습니다. 전방을 촬영할 파이 카메라를 2개 사용하고 1번 카메라는 차선 영상, 2번 카메라는 표지판, 신호등 인식에 사용하여 데이터를 아두이노로 전달하면 모터 출력을 제어하는 방식으로 제작할 예정입니다. 그리고 초음파 센서는 6개를 사용하여 6방향으로 설치하고 장애물 감지 시 아두이노에서 인터럽트를 걸어 정지할 수 있도록 합니다. 그리고 초음파 센서 데이터만으로 자율주차를 구현하려 합니다.

그리고 트랙은 위에 그림과 같이 주차, 터널, 곡선, 신호등, 경사, 회전교차로, 차량추월과 같은 항목들을 테스트할 수 있게 하드보드지와 종이, 노란 테이프로 제작합니다.

라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng
라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

제일 중요한 부분인데 영상처리 쪽으로는 처음 공부해봐서 많이 생소하고 어려울 것 같습니다. 대략적으로 조사한 내용으로는 원본 영상을 흑백 영상으로 만들고 그것을 가우시안 필터링을 통해 단순한 이미지로 변환하고 허프 변환을 사용해서 직선을 추출해내서 소실점을 찾는다고 합니다. 그 소실점 좌표를 통해서 조향을 컨트롤할 예정입니다. 그리고 표지판 같은 경우에는 템플릿 매칭이라는 방법이 있는데 비슷한 이미지를 찾아서 인식하는 기법입니다. 하지만 이미지의 크기가 달라지면 정확도가 많이 떨어진다는 단점이 있어서 경계선을 추출하거나 다른 방안을 조사해 봐야 할 것 같습니다.

<주행 알고리즘>

라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

알고리즘 관해서는 https://cccding.tistory.com/110여기 자료를 참고하였고 저기서 추가적으로 조도센서나 LED 제어, 그리고 주차 알고리즘도 추가할 예정입니다. 

라즈베리파이

라즈베리파이(영어: Raspberry Pi)는 영국 잉글랜드의 라즈베리 파이 재단이 학교와 개발도상국에서 기초 컴퓨터 과학의 교육을 증진시키기 위해 개발한 신용카드 크기의 싱글 보드 컴퓨터이다. 아두이노와 함께 IoT 분야의 주역으로 성장하였다.

라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

다음과 같이 싱글 보드 형태의 컴퓨터로 출시하였지만, 다양한 주변기기와 부속품을 통해 보다 많은 분야에서 활용되고 있다.

GPIO

GPIO는 General Purpose Input Output의 약자로 라즈베리파이와 전자적으로 통신하기위한 표준포트이다. GPIO를 통해 쉽게 전자회로를 통제하고 원하는 사물인터넷 기기를 만들 수 있다.

이번에 사용한 라즈베리 파이 모델 3의 경우는 총 40개의 핀이 있다.

라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

그 중 GPIO는 입력용과 출력용으로 사용할 수 있다. GPIO가 사용하는 표준 전압은 3.3V이고 이보다 높을 경우 보드가 손상을 입을 수 있다. GPIO를 통해 입력 또는 출력되는 전압이 대략 1.7V 이하이면 0, 이상이면 1로 인식한다. 이를 이용하여 주변 전자장치와 신호를 주고 받을 수 있다.

GPIO핀에는 5밀리암페어의 적은 전류만 공급되는데, 이를 직접 이용해서는 조그만 LED정도만 켜고 끌수 있다. 전원은 3.3V와 5V 두종류가 있고, GND는 접지용도로 사용된다. GPIO핀중 뒤에 수식어가 붙어서 특수용도로 사용되는 핀은 SDA, SCL이다. 시계와 데이터 회선용도로 사용가능하다. 블루투스 전자모듈처럼 직렬통신을 할 경우 수신용 RX와 송신용 TX포트가 있다. MOSI, MISO, SCK 또한 직렬 통신에 사용된다.

이와 같은 직렬 인터페이스를 직렬주변기기인터페이스버스(SPI)라고 부른다.

PWM핀은 모터와 LED를 제어할 떄 단순히 디지털로 껐다, 켰다로만 사용하는 것이 아니라 조금씩 강하게, 약하게 신호를 주고받을 수 있게 해준다. 자동차의 속도를 조절할때 PWM을 사용할 것이다.

라즈비안

라즈비안은 라즈베리파이에서 사용하는 OS이다. 라즈비안은 Linux를 기반으로 구성된 OS이다. 이번 프로젝트는 라즈비안을 이용하여 진행할 것이다. www.raspberrypi.org 홈페이지에서 라즈비안 img파일을 다운로드 받는다.

라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

포맷된 SD카드를 준비하고 Etcher 프로그램을 다운받는다.
라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

다음과 같이 Etcher프로그램을 통해 micro SD카드에 라즈비안 img파일을 설치한다.
라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

다음은 라즈비안을 라즈베리파이에 설치를 완료한 모습이다.
라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

라즈베리파이에 모니터와 마우스, 키보드를 연결하기 번거로우니 원격으로 컴퓨터를 제어할 수 있는 VNC를 사용하여 사용중인 PC로 제어한다.

라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

VNC Server란에 라즈베리파이가 사용중인 IP주소를 입력하고 이름을 입력한다.
라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

위와 같이 다른 PC를 이용하여 원격으로 라즈베리파이를 제어할 수 있다.

GPIO 제어하기

파이썬의 RPi.GPIO 모듈을 사용하여 GPIO 포트를 제어할 것이다.
라즈베리파이는 브로드컴사의 단일칩 시스템 SOC를 이용하고 있으므로 BCM 모드를 이용하여 핀 번호를 GPIO모듈 번호로 사용한다.
사용한 핀번호를 setup함수에 인자로 사용하여 그 번호를 출력용으로 사용하겠다고 선언한다.

import RPi.GPIO as GPIO

GPIO.setmode(GPIO.BCM)
GPIO.setup(18, GPIO.OUT)

L293D

모터를 제어하기 위해서는 L293D라는 모터 드라이브가 필요하다. 이 L293D 드라이브를 이용하여 모터의 방향과 세기를 조절할 것이다. 모터의 방향은 디지털 신호로 제어하고, 힘의 세기는 앞에서 설명한 PWM의 아날로그 값을 이용하여 조절한다.

다음은 모터를 제어하기위한 회로 구성도이다.

두개의 DC 모터에 각 두개씩 output선이 연결되어 있다. DC모터는 중심축의 코일에 전기를 보내서 주변의 영구자석간의 반발력을 이용하여 회전을 만드는 가장 일반적인 모터이다. 그러므로 전류의 방향과 세기를 다르게 하면 모터의 회전방향과 세기도 조정할 수 있다. 방향을 조절하기 위한 신호 두개와 세기를 조정하는 pwm이 최소하나 연결되어 있어야 한다.

L293D는 4.5V ~ 36V까지 전원을 공급받을 수 있다. 그러므로 라즈베리파이에서 제공하는 5V전원을 사용해야 한다. L293D IC칩의 8번과 16번에 전원을 공급하고 4번, 5번, 12번, 13번 네개의 핀은 접지에 연결한다.

각 모터마다 3개씩 총 6개의 핀을 L293D 칩에 연결해야 한다. 라즈베리파이의 GPIO 16번, 20번, 21번 포트와 13번, 19번, 26번 포트를 사용한다. 그 중 13번, 16번, 19번핀은 PWM핀으로 두 모터의 힘의 세기를 조절하는 PWM 용도로 사용한다. 이제 모터를 회로에 연결해야한다.. 하나의 모터를 L293D칩의 3번, 6번핀에 연결하고, 나머지 모터를 11번, 14번 핀에 연결한다.

HC-SR04 초음파 센서

HC-SR04 초음파 센서에는 총 4개의 핀이 있다. 한 쪽 끝의 VCC핀에는 라즈베리파이에 5V 전원을 공급하고,

GND핀은 접지용, TRIG핀은 라즈베리파이로부터 신호를 받는 역할, 신호를 받으면 초음파를 발사하고 다시 수신하여 이 값을 ECHO핀에 출력한다. 이 값을 라즈베리파이가 받아서 거리를 계산한다.

ECHO핀은 5V 전압을 사용하는데 라즈베리파이는 3.3V 전압을 사용하므로 1K옴 이상의 저항을 연결해주어야 한다.

다음은 초음파 센서를 연결하기 위한 회로도이다.

VCC와 GND는 브레드보드에서 모터에서 사용하는 입력값을 같이 사용하고 TRIG핀은 GPIO 23번핀에 연결한다. GPIO의 24번핀은 브레드보드의 저항을 거쳐서 ECHO핀에 연결한다.

코드를 살펴보자.

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)

먼저 GPIO라이브러리와 time라이브러리를 import하고, setmode함수를 이용하여 핀번호를 참조할 수 있도록 한다.

TRIG = 23                                  
ECHO = 24

GPIO.setup(TRIG,GPIO.OUT)                  
GPIO.setup(ECHO,GPIO.IN)

TRIG와 ECHO핀에 사용한 GPIO핀번호를 변수를 통해 초기화하고, 초음파 신호를 주고 받을 수 있도록 setup함수를 이용하여 설정한다.

def getDistance():
  GPIO.output(TRIG, False)                 
  time.sleep(1)  

  GPIO.output(TRIG, True)                  
  time.sleep(0.00001)                      
  GPIO.output(TRIG, False)

  while GPIO.input(ECHO)==0:               #Check whether the ECHO is LOW
    pulse_start = time.time()              #Saves the last known time of LOW pulse

  while GPIO.input(ECHO)==1:               #Check whether the ECHO is HIGH
    pulse_end = time.time()                #Saves the last known time of HIGH pulse 

  pulse_duration = pulse_end - pulse_start #Get pulse duration to a variable

  distance = pulse_duration * 17150        #Multiply pulse duration by 17150 to get distance
  distance = round(distance, 2)            #Round to two decimal points

  return distance

getDistance()함수를 이용하여 장애물간의 거리를 구한다. 1초간격으로 초음파 센서를 껐다 켜면서 초음파 센서를 작동하도록 한다. 껐다 켤때의 시간을 start 와 end 변수에 나눠 담고, 둘의 차를 계산한뒤 17150을 곱하여 거리를 측정한다.

if __name__ == '__main__':
  try:
    while True:
      distance_value = getDistance()
      if distance_value > 2 and distance_value < 400:      
          print ("Distance is %.2f cm" %distance_value)  #Print distance with 0.5 cm calibration
      else:
          print ("Out Of Range")                         #display out of range 


  except KeyboardInterrupt:
    print ("Terminate program by Keyboard Interrupt")
    GPIO.cleanup()

거리값이 2cm이상이고 400cm이하 일동안 거리를 출력한다. Ctrl + C를 입력하면 종료한다.

자율주행 기능 구현(장애물 회피)

이제 모터를 제어하고 장애물이 있으면 회피하는 기능을 구현하자.

import RPi.GPIO as GPIO                    
import time                                

#Set GPIO BCM(Broadcom SoC) pin number 
GPIO.setmode(GPIO.BCM)      

TRIG = 23                                  
ECHO = 24                                  

GPIO.setup(TRIG,GPIO.OUT)                  
GPIO.setup(ECHO,GPIO.IN)

RIGHT_FORWARD = 26                                  
RIGHT_BACKWARD = 19                                   
RIGHT_PWM = 13
LEFT_FORWARD = 21                                  
LEFT_BACKWARD = 20                                   
LEFT_PWM = 16 

왼쪽과 오른쪽 모터를 제어하기 위한 칩번호를 선언한다. 왼쪽과 오른쪽의 전진, 후진 그리고 PWM으로 구성되어 있다.

GPIO.setup(RIGHT_FORWARD,GPIO.OUT)                  
GPIO.setup(RIGHT_BACKWARD,GPIO.OUT)
GPIO.setup(RIGHT_PWM,GPIO.OUT)
GPIO.output(RIGHT_PWM, 0)
RIGHT_MOTOR = GPIO.PWM(RIGHT_PWM, 100)
RIGHT_MOTOR.start(0)
RIGHT_MOTOR.ChangeDutyCycle(0)

GPIO.setup(LEFT_FORWARD,GPIO.OUT)                  
GPIO.setup(LEFT_BACKWARD,GPIO.OUT)
GPIO.setup(LEFT_PWM,GPIO.OUT)
GPIO.output(LEFT_PWM, 0)
LEFT_MOTOR = GPIO.PWM(LEFT_PWM, 100)
LEFT_MOTOR.start(0)
LEFT_MOTOR.ChangeDutyCycle(0)

전진, 후진, PWM 핀번호를 출력용으로 설정하고 PWM은 모터의 주파수를 100으로 설정한다. start()와 ChangeDutyCycle()함수를 통하여 모터의 출력값을 0 ~ 100 %로 설정할 수 있다. 우선 0으로 초기화한다.

#Get distance from HC-SR04 
def getDistance():
  GPIO.output(TRIG, GPIO.LOW)                 
  time.sleep(1)                            

  GPIO.output(TRIG, GPIO.HIGH)                  
  time.sleep(0.00001)                      
  GPIO.output(TRIG, GPIO.LOW)

  #When the ECHO is LOW, get the purse start time
  while GPIO.input(ECHO)==0:                
    pulse_start = time.time()               
  
  #When the ECHO is HIGN, get the purse end time
  while GPIO.input(ECHO)==1:               
    pulse_end = time.time()                 

  #Get pulse duration time
  pulse_duration = pulse_end - pulse_start 
  #Multiply pulse duration by 17150 to get distance and round
  distance = pulse_duration * 17150        
  distance = round(distance, 2)           
 
  return distance

거리를 구하는 함수는 위의 설명을 참고하자.

#Right Motor Control 
def rightMotor(forward, backward, pwm):
  GPIO.output(RIGHT_FORWARD,forward)
  GPIO.output(RIGHT_BACKWARD,backward)
  RIGHT_MOTOR.ChangeDutyCycle(pwm)

#Left Motor Control 
def leftMotor(forward, backward, pwm):
  GPIO.output(LEFT_FORWARD,forward)
  GPIO.output(LEFT_BACKWARD,backward)
  LEFT_MOTOR.ChangeDutyCycle(pwm)

forward, backward, pwm을 파라미터로 받아 모터를 제어하는 함수를 선언한다.

if __name__ == '__main__':
  try:
    while True:
      distance_value = getDistance()
      #Check whether the distance is 50 cm
      if distance_value > 50:      
          #Forward 1 seconds
          print ("Forward " + str(distance_value))
          rightMotor(1, 0, 70)
          leftMotor(1, 0, 70)
          time.sleep(1)
      else:
          #Left 1 seconds
          print ("Left " + str(distance_value))
          rightMotor(0, 0, 0)
          leftMotor(1, 0, 70)
          time.sleep(1)
      
  except KeyboardInterrupt:
    print ("Terminate program by Keyboard Interrupt")
    GPIO.cleanup()

장애물과의 거리가 50cm 이상이면 1초동안 앞으로 전진한다. 여기서 rightMotor와 leftMotor함수에 forward, backward, pwm 값을 인수로 주어서 모터를 제어한다. 1이면 작동하고 0이면 정지한다.

다음은 조립을 완성한 미니카의 모습이다.

라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

작동하는 모습을 영상으로 찍어보았다.
https://youtu.be/oLcthl1rbQk

원격 모니터링

이제 파이썬 플라스크 웹서버를 이용하여 원격으로 라즈베리파이와 통신하고 제어할 수 있는 기능을 구현해 볼 것이다. 또한 라즈베리파이의 파이카메라를 연결하여 원격으로 실시간 영상을 볼 수 있는 기능 또한 구현 할 것이다.

라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

플라스크는 파이썬을 기반으로 작성된 웹 프레임워크의 하나로, Werkzeug 툴킷과 Jinja2 템플릿 엔진을 기반으로 하고 있다.

우선 플라스크를 이용하여 원격지에서 라즈베리파이와 통신하려면 DDNS를 이용해야한다.

라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

DDNS 설정

DDNS는 실시간으로 DNS를 갱신하는 방식이다. 인터넷 상에서 서버를 찾아가는 방법은 고유의 숫자인 IP이고 이것은 우리가 사용하는 www.google.com과 같은 이름을 호스트의 고유 네트워크 주소로 바꿔주는 서비스가 DNS이다. 우리가 집에서 사용하는 무선 공유기는 고정된 IP값을 사용하는 것이 아니라 인터넷 서비스 업체에서 제공하는 한정된 네트워크 주소 자원을 돌려서 사용하고 있는 것이다.

이렇게 바뀌는 주소를 유동 IP라고 한다.

이렇게 IP가 바뀌어도 도메인 이름과 연결시켜서 사용할 수 있게 해주는 것이 DDNS서비스이다.

이 서비스를 이용하여 고유의 도메인 이름을 사용하면 집에있는 무선 공유기의 내부 IP가 아무리 많이 바뀌어도 그 이름으로 찾아갈 수 있다.

이번 프로젝트에 사용할 도메인 이름으로 aga1000.asuscomm.com으로 설정하였다.

라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

포트 포워딩

보통 하나의 무선 공유기에 여러대의 기기를 접속하여 사용한다. 공유기는 외부에서는 하나의 IP를 받아서 인터넷에 접속하지만 내부에서는 내부 자체의 IP망을 구성하여 사용하고 있다. 이렇게 여러대의 장비가 하나의 외부 공용 IP를 같이 써서 외부 인터넷에 접속하여 사용한다.

무선공유기에 연결된 여러대의 기기들 중 특정한 기기에만 접속하려면 그 기기만의 내부 고유 IP를 알아야 한다. DDNS를 사용하여 외부에서 무선공유기 IP로 접속하여 내부의 여러 장비의 고유 IP중 어디로 접근해야할까. 공유기 안의 여러 장치중 라즈베리파이가 사용하는 내부 고유 IP로 안내하고 싶은데 어덯게 해야할까.

특정 내부 IP로 연결해주는 기능이 포트 포워딩(Port Forwading)이다.

라즈베리 파이의 고유 Port로 5561번을 설정해 주었다.

라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

코드 :

from flask import Flask, render_template, request, Response
import RPi.GPIO as GPIO                 
import time      
import io
import threading
import picamera

flask 라이브러리와 render_template, request, Response를 import하고, 스트리밍 영상 처리를 위한 io, 별도 프로세스 thread를 구현하기 위한 threading, 파이카메라를 사용하기 위한 picamera 라이브러리를 import 한다.

class Camera:
    thread = None  # background thread that reads frames from camera
    frame = None  # current frame is stored here by background thread
    start_time = 0  # time of last client access to the camera

스레드는 프로세스를 여러개로 나눈 독립적인 실행단위이다. 이번 원격 스트리밍 기능에서는 비디오도 송출하고 미니카도 조종해야 한다. 그런데 비디오를 송출하다가 미니카를 제어하는 명령을 늦게 처리한다면 사고가 날 수 있다. 따라서 별도의 독립적인 프로세스인 스레드 개념을 도입하여 비디오 송출과 미니카 조종 기능을 분리 하였다.

    def getStreaming(self):
        Camera.start_time = time.time()
        #self.initialize()
        if Camera.thread is None:
            # start background frame thread
            Camera.thread = threading.Thread(target=self.streaming)
            Camera.thread.start()

            # wait until frames start to be available
            while self.frame is None:
                time.sleep(0)
        return self.frame

    @classmethod

streaming 함수를 관리해주고 화면을 보내주는 함수

    def streaming(c):
        with picamera.PiCamera() as camera:
            # camera setup
            camera.resolution = (320, 240)
            camera.hflip = True
            camera.vflip = True

            # let camera warm up
            camera.start_preview()
            time.sleep(2)

            stream = io.BytesIO()
            for f in camera.capture_continuous(stream, 'jpeg',
                                                 use_video_port=True):
                # store frame
                stream.seek(0)
                c.frame = stream.read()

                # reset stream for next frame
                stream.seek(0)
                stream.truncate()

                # if there hasn't been any clients asking for frames in
                # the last 10 seconds stop the thread
                if time.time() - c.start_time > 10:
                    break
        c.thread = None

독립적인 스레드로 파이카메라에서 프레임 단위로 계속 영상을 보내주는 함수

app = Flask(__name__)

#Set GPIO BCM(Broadcom SoC) pin number 
GPIO.setmode(GPIO.BCM)      

TRIG = 23                                  
ECHO = 24                                  

GPIO.setup(TRIG,GPIO.OUT)                  
GPIO.setup(ECHO,GPIO.IN)

RIGHT_FORWARD = 26                                  
RIGHT_BACKWARD = 19                                   
RIGHT_PWM = 13
LEFT_FORWARD = 21                                  
LEFT_BACKWARD = 20                                   
LEFT_PWM = 16 

GPIO.setup(RIGHT_FORWARD,GPIO.OUT)                  
GPIO.setup(RIGHT_BACKWARD,GPIO.OUT)
GPIO.setup(RIGHT_PWM,GPIO.OUT)
GPIO.output(RIGHT_PWM, 0)
RIGHT_MOTOR = GPIO.PWM(RIGHT_PWM, 100)
RIGHT_MOTOR.start(0)
RIGHT_MOTOR.ChangeDutyCycle(0)

GPIO.setup(LEFT_FORWARD,GPIO.OUT)                  
GPIO.setup(LEFT_BACKWARD,GPIO.OUT)
GPIO.setup(LEFT_PWM,GPIO.OUT)
GPIO.output(LEFT_PWM, 0)
LEFT_MOTOR = GPIO.PWM(LEFT_PWM, 100)
LEFT_MOTOR.start(0)
LEFT_MOTOR.ChangeDutyCycle(0)

#Get distance from HC-SR04 
def getDistance():
  GPIO.output(TRIG, GPIO.LOW)                 
  time.sleep(1)                            

  GPIO.output(TRIG, GPIO.HIGH)                  
  time.sleep(0.00001)                      
  GPIO.output(TRIG, GPIO.LOW)

  #When the ECHO is LOW, get the purse start time
  while GPIO.input(ECHO)==0:                
    pulse_start = time.time()               
  
  #When the ECHO is HIGN, get the purse end time
  while GPIO.input(ECHO)==1:               
    pulse_end = time.time()                 

  #Get pulse duration time
  pulse_duration = pulse_end - pulse_start 
  #Multiply pulse duration by 17150 to get distance and round
  distance = pulse_duration * 17150        
  distance = round(distance, 2)           
 
  return distance

#Right Motor Control 
def rightMotor(forward, backward, pwm):
  GPIO.output(RIGHT_FORWARD,forward)
  GPIO.output(RIGHT_BACKWARD,backward)
  RIGHT_MOTOR.ChangeDutyCycle(pwm)

#Left Motor Control 
def leftMotor(forward, backward, pwm):
  GPIO.output(LEFT_FORWARD,forward)
  GPIO.output(LEFT_BACKWARD,backward)
  LEFT_MOTOR.ChangeDutyCycle(pwm)

#Forward Car
def forward():
    rightMotor(1, 0, 70)
    leftMotor(1, 0, 70)
    time.sleep(1)

#Left Car
def left():
    rightMotor(0, 0, 0)
    leftMotor(1, 0, 70)
    time.sleep(0.3)

#Right Car
def right():
    rightMotor(1, 0, 70)
    leftMotor(0, 0, 0)
    time.sleep(0.3)

#Stop Car
def stop():
    rightMotor(0, 0, 0)
    leftMotor(0, 0, 0)#Forward Car
def forward():
    rightMotor(1, 0, 70)
    leftMotor(1, 0, 70)
    time.sleep(1)

#Left Car
def left():
    rightMotor(1, 0, 70)
    leftMotor(0, 0, 70)
    time.sleep(0.3)

#Right Car
def right():
    rightMotor(0, 0, 70)
    leftMotor(1, 0, 70)
    time.sleep(0.3)

#Backward Car
def backward():
    rightMotor(0, 1, 70)
    leftMotor(0, 1, 70)
    time.sleep(0.3)

#Stop Car
def stop():
    rightMotor(0, 0, 0)
    leftMotor(0, 0, 0)

@app.route("/<command>")
def action(command):
    distance_value = getDistance()
    if command == "F":
        forward()
        message = "Moving Forward"
    elif command == "L":
        left() 
        message = "Turn Left"
    elif command == "R":
        right()   
        message = "Turn Right"
    elif command == "S":
        stop()   
        message = "Stop"  
    elif command == "B":
        backward()   
        message = "Moving Backward"
    else:
        stop()
        message = "Unknown Command [" + command + "] " 

    msg = {
        'message' : message,
        'distance': str(distance_value)
    }
        
    return render_template('video.html', **msg)

route함수의 파라미터로 command를 사용하여 웹 서버를 통해 html 페이지로 명령을 보낼 수 있다. getDistance()함수를 이용하여 장애물과의 거리를 측정한 후 각 command에 맞는 함수를 실행하여 미니카를 제어한다.

msg라는 배열에 메시지와 거리값을 render_template함수를 이용하여 html파일에 전달한다. render_template함수를 사용하면 html 문서를 렌더링 할 수 있다.

def show(camera):
    """Video streaming generator function."""
    while True:
        frame = camera.getStreaming()
        yield (b'--frame\r\n'
               b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')

Camera 클래스에서 프레임을 계속 보내주는 getStreaming함수의 결과화면을 받아서 html에서 표시가능한 형식으로 표현하는 역할을 하는 함수

@app.route('/show')
def showVideo():
    """Video streaming route. Put this in the src attribute of an img tag."""
    return Response(show(Camera()),
                    mimetype='multipart/x-mixed-replace; boundary=frame')

show함수에서 받아온 화면을 html파일에 response하는 기능을 한다.

if __name__ == "__main__":
    try:
        app.run(host='0.0.0.0', port=5561, debug=True, threaded=True)
    except KeyboardInterrupt:
        print ("Terminate program by Keyboard Interrupt")
        GPIO.cleanup()

port를 포트 포워딩한 5561로 설정하고 실행하였다.

나의 라즈베리파이의 고유 내부 IP주소인 aga1000.asuscomm.com:5561/S로 연결하여 RC카를 작동해본 모습이다.

라즈베리파이 영상처리 자율주행 - lajeubelipai yeongsangcheoli jayuljuhaeng

작동 영상 :
https://youtu.be/qo87FHvM9Cw

느낀점

이번 프로젝트를 통해 소프트웨어와 하드웨어를 결합하는 경험을 해볼 수 있었고, 너무 흥미로운 시간이었다. 그동안 이번 프로젝트만큼 결과물을 직접 눈으로 확인할 수 있는 프로젝트는 적었어서 더 재미있었던 프로젝트였다. 이번 프로젝트에서는 장애물을 만나면 1초동안 회피하는 동작에서 그치지만, 앞으로 더 나아가 영상인식, 머신러닝 기술을 도입한 진정한 자율주행 자동차를 만들어 보고 싶어졌다. 자율주행 분야로 진출하고 싶은 나의 의지를 확인할 수 있었고, 아직 많이 부족하지만 나의 꿈인 자율주행 소프트웨어 개발자가 되기위해 피나는 노력을 할 것이다!

참고강의

본 게시물을 creapple사이트의 파이썬 라즈베리파이 IoT프로젝트-원격모니터링 자동차 강의를 수강하고 작성하였습니다.
https://www.creapple.com/item