일사분란착착착 - 영상인식을 통한 RVM 시스템 개발

MIE capstone
2012430017 (토론 | 기여)님의 2020년 6월 21일 (일) 11:11 판
이동: 둘러보기, 검색

프로젝트 소개

프로젝트 명

영상인식을 통한 RVM 시스템 개발
Developing Reverse Vending Machine Using Image Detection

프로젝트 기간

2020.3 ~ 2020.6

팀 소개

서울시립대학교 기계정보공학과 (20144300**) (유*영) (팀장)
서울시립대학교 기계정보공학과 (20144300**) (강*찬)
서울시립대학교 기계정보공학과 (20144300**) (박*석)
서울시립대학교 기계정보공학과 (20144300**) (심*헌)
서울시립대학교 기계정보공학과 (20144300**) (윤*상)

소스코드

https://github.com/YeonsangYoon/embedded-system-project

프로젝트 개요

프로젝트 요약

프로젝트의 배경 및 기대효과

페트병은 재활용 자원 중에서도 재활용 가능성이 높고 또 재활용 후에도 품질이 크게 떨어지지 않는다. 하지만 라벨 및 뚜껑이 있는 경우 재활용 전처리 과정에서 비용과 시간이 크게 늘어가게 된다. 하지만 페트병의 라벨 및 뚜껑을 제거하여 버림이 올바른 방법임을 모르는 경우 많다. 이를 위해 올바른 재활용 방법 홍보가 필요하지만 실용적으로 이루어지지 않는 상태이다. 우리는 사람들에게 조금 더 흥미로운 방법을 통해서 재활용 방법을 홍보하기 위해 독일 덴마크 등에서 활용되는 RVM을 모티브로 삼았다. 또한 제도적으로 RVM이 확립되어 있지않은 상태에서 더 많은 페트와 캔을 수거하기 위해 영상인식 RVM을 생각하였다. RVM(reverse vending machine)은 일반 자판기와는 다르게 돈을 넣고 물건을 받는 것이 아닌, 물건은 넣으면 돈이 나오는 구조이다. 페트나 캔을 넣으면 소정의 보상을 받게되고 이 과정에서 자연스럽게 흥미가 생긴다. 동시에 올바른 재활용 방법을 익히는 교육적인 효과도 불러올 것이다. 우리는 기존의 RVM과 달리 임베디드 시스템을 활용하여 더 경제적이고 이동식인 시스템을 구축함을 목표로 했다. 기존의 RVM이 크기 및 무게 문제로 인해 설치 장소에 고정이 되어있는것과 달리 우리가 개발한 시스템은 여러장소(초등학교, 주거단지 등)에 유연하게 배치하여 소량의 기기로 큰 교육효과를 누릴수 있을 것이다.

동작 시나리오

그림 1. 사용자 시나리오

본 프로젝트에서 구현을 목표로 한 부분은 검은색 글씨로 표기하였다. 기존의 RVM 시스템은 사용자들의 적극적인 재활용쓰레기 수거를 위하여 투입한 쓰레기에 비례해 일정부분 현금이나 포인트로 보상을 주었다. 하지만 본 프로젝트에서 짧은 기간내 기본적인 RVM 기능 외 어플리케이션과 사용자 포인트 적립을 구현하는 것이 힘들다고 판단하여 부가기능은 추후에 개발하고 기본적인 RVM의 기능을 수행하는 시스템을 개발을 목표로 잡았다. 기본적인 사용자 시나리오는 다음과 같다. 사용자가 시작버튼을 누르면 내부적으로 기계의 상태가 ON으로 바뀌어 동작 시퀀스가 실행된다. 기본적으로 한번에 1개의 쓰레기를 처리하도록 동작을 하며 내부적으로 투입 -> 이동 -> 판별 -> 분류 의 순서로 동작한다. 사용자가 모든 재활용 쓰레기를 투입하여 종료버튼을 누르면 내부적으로 기계의 상태가 OFF로 바뀌어 동작 시퀀스가 완료된 후 idle 상태로 바뀌고 투입 결과가 UI에 표시된다.

구현 내용

시스템 구성

그림2. 시스템 구성도
  • RVM Controller
  • Image Processing Server
  • Rail Controller

RVM 시스템은 크게 3개의 프로세스로 구동된다. RVM Controller는 UI를 통해 사용자 이벤트를 처리하고 Status와 Main Cycle을 통해 시스템 상태를 관리하고 기계를 동작시키는 역할을 한다. RVM Controller는 하나의 프로세스로서 라즈베리파이에서 작동하며 Main Cycle과 UI를 2개의 Thread로 나누어 동시에 실행된다. UI는 사용자의 Event를 받아 Machine Status를 바꾸고 Main Cycle은 현재 status에 따라 전체 시스템을 동작시킨다. Rail Controller는 모든 모터를 제어하여 쓰레기를 판별하는 위치까지 이동시키고 각 결과에 따라 분류하도록 하는 프로세스이다. RVM Controller에게 Serial 통신을 통해 명령을 전달받아 아두이노에서 작동한다. Rail Controller는 총 4가지 동작 Case를 처리한다. 마지막으로 Image Processing Server는 판별부까지 이동한 쓰레기의 사진을 찍어 영상처리를 통해 판별하는 프로세스이다. RVM Controller와 Htttp 통신을 하기 위해 Web Server를 구축하였다. RVM Controller에서 판별 request를 보내면 Image Server에서는 사진을 찍고 영상처리를 하여 결과를 다시 보내준다.

하드웨어 설계 및 구현

RVM의 아두이노 메가 2560은 전반적인 모터제어 및 라즈베리파이와 시리얼 통신을 통해 동작 요청과 완료 신호를 송수신을 한다.

그림3. 하드웨어 연결도
  • 기어박스2.PNG
  • 그림4.1 기어박스
  • 그림4.2 레일 & 투입부
  • 그림4.3 페트병 분류기

아두이노 메인루프 : 서버역할을 하는 라즈베리파이와 시리얼 통신을 하고 들어오는 신호에 따라 정해진 기구부를 작동 및 제어한 후 해당 동작을 완료하면 서버에 동작 완료 신호를 보냅니다.

void loop()
{
    if(Serial.available() > 0)
    {
      in_data = Serial.read();
      switch(in_data)
      {
    
          case '1' :  // 입구 동작
            M2_duration = 0;
            M3_duration = 0;
            stepM(stepsPerRevolution*-1.3);
            M1_CW(225,1500);
            Serial.write('y');
            stepM(stepsPerRevolution*1.3);
            Flush();
            break;
    
          case '2' : // pet 분류 처리
            M2_duration = 0;
            M3_duration = 0;
            M3_CW(900);
            M2_duration = 0;
            M3_duration = 0;
            M1_CW(220,800);
            M3_CCW(900);
            Serial.write('y');
            Flush();
            break;
  
          case '3' :  // can 분류 처리
            M2_duration = 0;
            M3_duration = 0;
            M2_CW(5500,250);
            delay(100);
            M2_duration = 0;
            M3_duration = 0;
            M2_CCW(5400,250);
            Serial.write('y');
            Flush();
            break;
  
          case '4':  // 반송 처리
            M2_duration = 0;
            M3_duration = 0;
            M1_CCW(255,2500);
            Serial.write('y');
            Flush();
            break;
    
          default :
            Serial.write('E');
            Flush();
            break;
      }
  }
}

void Flush()    //Serial통신 버퍼 제거
{
    while(Serial.available() > 0)
    {
        Serial.read();
    }
}

3개의 dc모터, 1개의 스탭모터, 2개의 모터 드라이버가 사용됐습니다. 각각의 모터의 속도, 방향 그리고 엔코더 값에따라 모터를 제어 할 수 있습니다. 4가지의 모터의 방향 및 속도를 제어, 두 개의 dc모터는 엔코더센서를 통해 회전 정도를 제어할 수 있습니다.

// DC Motor 1 : rail
//a = enc , b = speed (0~255)
void M1_CW(int b, int c)
{
    digitalWrite(in1,HIGH);
    digitalWrite(in2,LOW);
    analogWrite(analog, b);      
    delay(c);
    M1_stop();
}

//a = enc , b = speed (0~255)
void M1_CCW(int b, int c) {
    digitalWrite(in1, LOW);
    digitalWrite(in2, HIGH);
    analogWrite(analog, b);      
    delay(c);

    M1_stop();
}

void M1_stop()
{
    digitalWrite(in1, LOW);
    digitalWrite(in2, LOW);
    delay(500);
}


//DC Motor 2
//a = enc , b = speed (0~255)
void M2_CW(int a, int b) {

    int temp = 0;
    int temp1 = 0;
    int count = 0;
    while(abs(M2_duration) <= a)
    {
        digitalWrite(M2_in1,HIGH);
        digitalWrite(M2_in2,LOW);
        analogWrite(analog2, b);   
        
        delay(5);

        temp = M2_duration;
        if(temp==temp1)
        {
          count++;
          if(count>150)
              break;
        }
        else
        {
          count = 0;
        }
        temp1 = M2_duration;
    }
    M2_stop();
}

//a = enc , b = speed (0~255)
void M2_CCW(int a, int b) 
{
    int temp = 0;
    int temp1 = 0;
    int count = 0;

    digitalWrite(M2_in1, LOW);
    digitalWrite(M2_in2, HIGH);
    
    while(abs(M2_duration) <= a)
    {
        analogWrite(analog2, b);
        delay(5);

        temp = M2_duration;
        if(temp==temp1)
        {
            count++;
            if(count>150)
                break;
        }
        else
        {
            count = 0;
        }

        temp1 = M2_duration;
    }
    M2_stop();
}

void M2_stop() {
    digitalWrite(M2_in1, LOW);
    digitalWrite(M2_in2, LOW);
    M2_duration = 0;      
}


//DC Motor 3 : Exit
//a = enc 
void M3_CW(int a)
{
    int temp = 0;
    int temp1 = 0;
    int count = 0;

    digitalWrite(M3_in1,HIGH);
    digitalWrite(M3_in2,LOW);
    
    while(abs(M3_duration) <= a){
        delay(5);
        temp = M3_duration;
        if(temp==temp1)
        {
            count++;
            if(count>100)
                break;
        }
        else
        {
            count = 0;
        }
        temp1 = M3_duration;
    }
    M3_stop();
}

//a = enc , b = speed (0~255)
void M3_CCW(int a)
{
    int temp = 0;
    int temp1 = 0;
    int count = 0;

    digitalWrite(M3_in1,LOW);
    digitalWrite(M3_in2,HIGH);

    while(abs(M3_duration) <= a){
         //Serial.print("M3_duration : ");
         //Serial.println(M3_duration);
         
        delay(5);
        temp = M3_duration;
        if(temp==temp1)
        {
            count++;
            if(count>100)
              break;
        }
        else
        {
            count = 0;
        }
        temp1 = M3_duration;
    }
    M3_stop();
}

void M3_stop() 
{
  digitalWrite(M3_in1, LOW);
  digitalWrite(M3_in2, LOW);
  // Serial.println("3 : stop");
  // delay(500);
  M3_duration = 0;      
}

//Step Motor1 : Entrance
void stepM(int stepsPerRevolution)
{
  myStepper.step(stepsPerRevolution);
}


소프트웨어 설계 및 구현

RVM의 소프트웨어는 기본적으로 python, C, C++을 사용하여 프로그래밍하였다. 아래의 그림은 내부적으로 3개의 프로세스들이 동작하는 시퀀스를 나타낸다. 사용자가 시작 버튼을 누르면 기계 상태가 On으로 바뀌며 Main Cycle을 시작하게 된다.

그림5. 내부 동작 시퀀스 다이어그램


RVMController

RVM Controller는 라즈베리파이에서 작동하는 프로세스이다. 파이썬으로 작성되었으며 pyqt5 파이썬 GUI 라이브러리를 사용하여 기본 골격을 만들었다. 거기에 현재 기계의 상태를 나타내주는 RVM Status Class와 각 동작을 실행하게 하는 Main Cycle을 구현하였다. 각 프로세스들이 여러가지 방법을 통해 통신하기 때문에 RVM Controller는 처음 시작할 때 여러가지의 통신 인터페이스를 초기화하고 각 센서 측정을 위한 GPIO setting을 해야한다. 아래의 코드는 RVM Controller의 main thread로서 기계를 처음 킬 때 Status class 인스턴스를 생성하고 각종 인터페이스를 초기화하고 로드셀 영점을 조절한다.

# stat class init
RVM_status = RVM_Stat() 

if not debug:
    # USB serial interface
    port = '/dev/ttyACM0'                           
    ser = serial.Serial(port, 9600, timeout = 2)

    # Load Cell GPIO setting
    GPIO.setwarnings(False)
    hx711 = HX711(LC_DT_Pin, LC_SCK_Pin)
    hx711.reset()

    w = []
    for i in range(20):
        w.append(hx711._read())

        if False in w:
            w.remove(False)
        
    w.remove(max(w))
    w.remove(min(w))
    init_avg = sum(w) / len(w)

    # IR Sensor GPIO setting
    GPIO.setup(IR_Pin1,GPIO.IN)
    GPIO.setup(IR_Pin2,GPIO.IN)
    GPIO.setup(IR_Pin3,GPIO.IN)
    GPIO.setup(IR_Pin4,GPIO.IN)

# main Cycle init
t1 = threading.Thread(target = main_Cycle)
t1.daemon = False
t1.start()

# 유저 인터페이스 init
app = QApplication(sys.argv) 
phone_window = phoneWindow() 
main_window = mainWindow()
main_window.showFullScreen()
app.exec_()
그림6. RVM Status & Main Cycle

RVM Controller는 전체 시스템을 관리하는 python 프로세스로서 UI, Main cycle, stat class의 인스턴스를 가지고 있다. Main cycle에서 단계별로 각 모듈을 함수 형태로 호출하며, 현재 stat을 관리한다. RVM Status는 기계의 온오프를 나타내는 Machine Status, 현재 실행 상태를 나타내는 Execute Status, 현재 투입된 재활용 쓰레기 정보인 Recycling Status, 에러 발생 여부를 나타내는 Error Status를 맴버로 가지고 있다. 또한 Main Cylce은 총 7단계의 실행 단계를 가지며 각 단계를 모두 실행해야 한개의 쓰레기가 처리된다.

def main_Cycle():
    # main cycle
    while 1:

        #0 machine stat check
        if RVM_status.machine_stat != RVM_STATE_ON:
            time.sleep(0.1)
        else :
            #1 Check IR sensor
            if checkObjectCond() < 0:
                if RVM_status.machine_stat == RVM_STATE_OFF :
                    resultD = 0;
                else : 
                    errorExit()

            #2 Check Load cell 
            elif checkLoadCell() < 0:
                errorExit()

            #3 rail move command 
            elif moveCommand('Dzone') < 0:
                errorExit()

            #4 Request discrimination
            else :
                resultD = requestD()
                print(resultD)

                #5 rail move command 
                if moveCommand(resultD)<0:
                    errorExit()

            if RVM_status.error_stat == retValOK:
                # Update Status
                RVM_status.updateStatus(resultD)
                main_window.can_pet()
                main_window.button_text()

            elif RVM_status.error_stat == Error :
                #debug msg
                while RVM_status.error_stat == Error :
                    continue

                printU("Error fixed.. restart")
                main_window.button_text()

RailController

Image Processing Server

Image Processing Server는 판별부에 도착한 물체를 영상인식을 통해 판별하는 역할을 한다. RVM Controller는 python requests 모듈을 통해 간단하게 Http request를 보낼 수 있다. 따라서 RVM Controller로부터 Http request를 받아 판별을 하기 위해 간단한 웹서버를 구축하였다. 웹서버는 임베디드 보드에서 충분히 사용할 수 있는 Flask라는 웹 프레임워크를 사용하였다. 서버는 Http request를 받게되면 총 3가지 단계를 통해 판별을 수행한다. 카메라 촬영 & 전처리 -> Yolo 영상인식 -> 결과 parsing의 순서로 동작하며 이에 대한 자세한 내용은 영상인식 파트에서 설명하겠다.

영상인식 구현

본 프로젝트에서 투입 물건은 카메라로 촬영할 수 있는 장소에 페트병, 캔 두 종류가 오게된다. 페트병과 캔의 분류는 실시간 물체 감지 시스템인 YOLOv3를 사용한다. 물건의 분류는 크게 3가지 단계로 이루어진다. 사진촬영 및 전처리, YOLOv3 실행, 파싱을 통한 결과값 확인

1단계 - 사진촬영 및 전처리

우선 카메라를 통해 촬영되는 이미지를 그대로 사용하기에는 문제가 있었다. 카메라를 통해 보이는 물체는 분류를 하려는 물건만 있는 것이 아니고 모터와 기어박스 등 여러가지 물체들이 존재했다. 때문에 일말의 오류를 방지하고자 오픈소스인 OpenCV를 사용하여 이미지를 필요한 부분만 추출하는 전처리 과정을 넣었다.

import os
import cv2

def imagepreprocess():
    """
    확장자가 jpg인 파일을 찾아서 불러오고 원하는 크기로 이미지를 자른다.
    자른 이미지는 기존 이미지를 덮어씌워서 저장한다.
    """
    path = "./"
    filenames = os.listdir(path)
    image_name = ""
    for filename in filenames:
        full_filename = os.path.join(path, filename)
        ext = os.path.splitext(full_filename)[-1]
        if ext == '.jpg': # find jpg
            image_name = full_filename[2:]

    image = cv2.imread(image_name, cv2.IMREAD_COLOR) # image load
    image_cut = image[76:390, :, :] # image cut
    cv2.imwrite(image_name, image_cut)


2단계 - YOLOv3

YOLOv3의 실행은 파이썬의 subprocess를 이용하였다. 그동안 학습시킨 가중치를 사용하여 전처리가 끝난 이미지를 불러와서 페트병과 캔이 검출이 되었는지 확인을 한다.

cmd_yolo = "./darknet detector test data/obj.data cfg/yolov3.cfg backup/yolov3_last.weights rvm/image/*.jpg >> detect.txt"

# Run Yolo
try:
    subprocess.call(cmd_yolo, shell=True, timeout=10)
except subprocess.TimeoutExpired: 
    print("Timeout during execution")
    return -1


3단계 - 파싱을 통한 결과값확인

일정패턴으로 나오는 텍스트를 통해서 원하는 문자를 파싱하여 결과값을 확인하였다.

UI구현

RVM은 기계의 상태를 모니터링 하고 조작하기 위한 터치스크린 UI를 포함하고 있다. 라즈베리 파이에서도 안정적으로 동작하는 가벼운 프로그램으로 제작하기 위해 python pyqt5를 이용하여 제작하였다. python으로 작성하여 메인 프로세스인 RVM_controller과 같은 프로세스에 동작하며 데이터 통신 비용을 낮추고 pyqt5를 이용하여 그래픽 부담이 낮은 UI를 구현하였다. UI는 아래에 표시된 3개의 화면으로 구성된다.


  • 그림7.1 시작화면
  • 그림7.2 동작화면
  • 그림7.3 종료화면


왼쪽화면은 시작화면으로 RVM이 동작 중이지 않다는 것을 나타낸다. 시작 버튼으로 RVM을 동작 상태로 만들 수 있다. 가운데 화면은 RVM의 동작중 화면으로 RVM의 현재 동작 단계를 표시하는 메시지창과 투입된 페트 또는 캔의 개수를 표시하는 창으로 구성된다. 종료 버튼을 눌러 종료화면으로 넘아 갈 수 있다. 오른쪽 화면은 종료화면으로 시작부터 종료까지 넣은 페트병과 캔의 총 개수를 표시해주며 기계를 종료한다. 종료후에는 시작화면으로 돌아간다.

프로젝트 결과

최종 결과물

그림8. 최종 결과물

시연영상

페트병 처리

파일:페트병.mp4

파일:캔.mp4

반송

파일:반송.mp4

무게초과

파일:무게초과.mp4

미구현 내용

프로젝트 평가

평가항목

평가결과

느낀점

박*석 - 이번 프로젝트를 하면서 하드에어 제작하는 것이 생각보다 어렵다는 것을 느꼈습니다. 아크릴, 풀리 등의 재료구매부터 각종 모터들과 센서들의 회로 구성까지 쉽지않았습니다. 팀원들과 같이 협력하면서 어려가지 문제들을 하나씩 해결하는 과정자체가 보람찼습니다. 페트병, 캔 분류에서는 YOLOv3 오픈소스를 사용하였는데 단순하게 데이터만 넣어서는 분류가 잘 되지않았습니다. 질 좋은 데이터들과 분류하려는 데이터들의 비율을 일정하게 맞추는 것이 물체 인식부분에 도움이 된다는 것을 새로 알았습니다. 이번 프로젝트 동안 같이한 팀원들에게 정말 고생 많았다고 말하고 싶습니다.