자율주행 시스템 thinkingo 구조

이 글은 프로그램 구조를 설명하는 글입니다. 저는 PAMS 2018 이라는 행사에 참여했던 한 대학생입니다. PAMS는 서울아트마켓이 아니라 판교자율주행모터쇼입니다. 11월 16, 17일에 해당 행사의 세부 프로그램으로 대학생 자동차 융합기술 경진대회 자율주행 부문이라는 것이 있었는데 그 대회에 출전했고, 4등을 했습니다1. 비록 수상은 못 했지만 제가 맡았던 부분을 정리해두면 누구든 읽을 것 같아서 일단 정리해 두겠습니다. 제가 맡은 부분은 전체 시스템 구조 설계 및 구현모니터링 시스템 개발이었습니다. 개발 기간은 딱 3주간이었습니다. 따라서 주먹구구식 개발이 많지만, 나름 고민한 흔적이 있기에 남겨봅니다.

개발 환경

하드웨어 간 연결 인터페이스가 이미 전부 구성되어있는 환경에서 개발하였기 때문에 application level 에서만 구현하였습니다. 프로세서는 평범한 조립 컴퓨터입니다. 파이썬을 사용한 이유는 빨리 개발해야 했고, 팀원들이 다들 쓸 줄 알았기 때문입니다2.

ThinkinGo 구조

프로젝트 이름 thinkingo는 thinking 과 Kingo 를 합친 단어입니다. Kingo는 성균관대학교의 상징인 은행잎을 뜻합니다3.

thinkingo-2018-11

이 그림은 전체 구조입니다. 급하게 그려서 조잡하지만 봐주십시오. 다시 깔끔하게 그릴 만큼 애정이 있지는 않습니다. 간단한(?) 학부 수준의 자율주행 대회였기 때문에, 전체 시스템도 역시 가장 간단한 파이프라인인 인지-판단-제어 모델로 설계하고 각 부분을 수행하는 데에 필요한 세부 루틴을 정의했습니다. 인지는 각 센서들의 정보를 얻어오고, 처리합니다. 판단은 센서로 판단한 주변 환경을 통해 자동차가 진행해야 할 적절한 경로를 생성합니다. 제어는 그 경로에 따라 차의 속도와 방향을 제어합니다. 아래에서 이 시스템의 디자인 철학에 대해 이야기하고, 각 세부 루틴이 하는 일을 간략히 말씀드리겠습니다.

또한 이 그림은 처음 설계할 때부터 그린 그림은 아니고, 최종 구현을 마치고 난 후에 이 프로젝트에서 실제로 구현된 내용을 설명하기 쉽도록 그린 그림입니다. 처음 설계할 때 그린 그림과는 달라졌지만 전체적인 컨셉은 비슷하게 유지하며 구현 단계에서 수정이 조금 있었습니다. 따라서 구조를 설명드리며 구현하면서 달라진 부분에 대해서도 말씀드리도록 하겠습니다.

Global Data Space

이건 제가 5월 대회 끝나고 회고하면서 ‘아 이렇게 할걸…‘을 반영한 부분입니다. 데이터를 전역변수로 관리하지 않으면서도 모든 세부 루틴이 트랜잭션을 만족하면서 접근할 수 있게 하는 방법을 찾고 싶었습니다. 그러면서 데이터 접근이 느려져서는 안 됩니다. 요구 사항을 정리하면 다음과 같습니다.

  1. 모든 세부 루틴이 같은 데이터 공간에 접근해야 한다.
  2. 자율주행 시스템은 I/O를 포함한 모든 연산을 예측 가능한 짧은 시간 범위 안에서 수행해야 한다.
  3. 트랜잭션이 꼬이면 안 된다. 즉, 어떤 세부 루틴이 데이터를 부분적으로 변경하고 있는 도중에 다른 세부 루틴이 그것을 사용해서는 안 된다. 데이터 간 의존성이 있다면 의존하는 것끼리의 변경이 모두 있을 때까지 그것과 관련된 데이터를 사용하는 세부 루틴이 있어서는 안 된다. 개떡같이 말해도 찰떡같이 알아들으실 수 있으리라 믿습니다.

처음에 생각했던 후보 중 하나는 진짜 DB를 구축하고 자율주행 어플리케이션이랑 소켓 같은 걸로 통신하게 하는 거였는데, 구현도 너무 복잡할 것 같고 I/O 오버헤드가 크지는 않을까 해서 우려하고 있었습니다. (얼마나 큰지는 정확히 모르겠습니다. 그냥 감으로…) 그래서 쉬이 구현을 못 하고 이것저것 검색해보며 하루 정도 더 고민하고 있었는데, 파이썬은 어떤 객체를 다른 객체를 생성할 때 매개변수로 넣어주면 그 넣어준 객체가 다른 객체 안에서 마치 레퍼런스처럼 작동한다는 사실을 알게 되었습니다. 그래서 조금 이상해 보이지만 아무튼 원하는 기능을 달성하는 다음 구조를 만들었습니다:

데이터 클래스를 설계한 뒤 그 인스턴스 하나를 찍어내 모든 세부 루틴 클래스 생성자에 매개변수로 전달하는 방식으로 데이터 공간을 공유한다.

클래스는 getter/setter등을 이용해 원하는 대로 데이터 I/O를 설계하기 편하다는 장점도 있었기 때문에, 일단 이렇게 해 보기로 했습니다. 진짜 DB처럼 과거의 데이터를 저장하는 기능은 필요성을 당장 느끼지 못해 구현하지 않았습니다. 대신 필요한 일부 데이터를 모니터링 화면에 띄운 뒤 그 화면 전체를 녹화하는 기능은 구현했습니다. 이는 글 아래 부분 모니터링 시스템 에서 자세하게 설명합니다.

Global Data Space 구현 특징 (data_class.py)
  1. 모든 세부 루틴이 같은 데이터 공간에 접근합니다. (공유 데이터라고 칭하겠습니다.)
  2. 각 세부 루틴이 공유 데이터에 접근하는 시간이 아마 매우 짧은 것 같습니다. (측정은 안 해봤습니다…)
  3. 트랜잭션이 꼬이는 경우가 있을 법도 한데, 크게 고려하지 않고 구현한 뒤 테스트 해 보니까 크게 눈에 띄는 버그가 없어서 일단은 그대로 구현했습니다. (…)
  4. 계속해서 업데이트되는 공유 데이터들을 시간 순서에 따라 어딘가에 재현 가능하게 저장하는 기능은 없습니다. (단순히 관찰 용도로 저장하는 기능이 있습니다.)

Data Source

이 구조는 처음 설계 시에는 계획에 없었던 구조였습니다. 그러나 11월 4일에 연습 주행을 다녀 온 이후, 로깅4해 온 데이터들을 이용해 자율주행 알고리즘 검증을 해야 할 필요성이 생기면서 고민 끝에 구현한 구조입니다. (제가 구현한 것은 아니고 팀장님이 구현했습니다.) 센서 데이터를 받는 부분을 간편한 인터페이스로 따로 설계하면 실제 센서를 연결할 수 없을 때 녹화된 데이터로 바꿔 끼워서 사용할 수 있습니다. 이렇게 하면 전체 어플리케이션 코드에는 영향을 끼치지 않으면서 간단하게 실험하기가 좋습니다. 따라서 저희는 그 인터페이스를 data_source.py 라는 이름으로 구현하기로 결정했습니다. 녹화된 데이터를 끼우는 데 사용하는 인터페이스는 test 디렉토리에 dummy_data_source.py 라는 이름으로 구현하였습니다.

Data Source 구현 특징 (data_source.py, dummy_data_source.py)
  1. 진짜 센서 데이터 data_source.py 와 녹화해 온 센서 데이터 dummy_data_source.py 를 바꿔 가며 실험할 수 있습니다.
  2. 웹캠과 LiDAR의 데이터를 각각 쓰레드를 이용해 스트림5 으로 관리합니다. (캠 3대, 라이다 1대 총 4 대의 센서)

Subroutines

같은 데이터 공간에 접근해야 하므로 자연스럽게 모든 세부 루틴은 쓰레드로 구현했습니다. 모든 세부 루틴의 구조는 동일합니다. 먼저, loop 를 포함한 함수를 가지고 있고 이것이 그 루틴의 메인 함수입니다. 그리고 생성자에서 앞서 말씀드린 공유 데이터 객체를 받습니다. 모든 개발자가 이 구조로 개발할 수 있도록 부모 클래스를 작성하여 이 클래스를 상속받아 작성하도록 했습니다. 이 링크에서 해당 구조를 확인할 수 있습니다.

이렇게 설계된 각 루틴은 전체 메인 코드에서 쓰레드로 계획된 뒤 실행되었습니다. 이 링크에서는 전체 메인 코드를 확인할 수 있습니다. 모든 쓰레드는 database 라고 이름 붙인 공유 데이터 객체에 접근 가능합니다.

쓰레드를 사용하긴 했지만 작동 방식을 세심하게 계획한 것은 아닙니다. 어차피 한 번 쓰고 말 프로그램 가능한 한 돌아가게만 3주 안에 만드는 게 목적이기도 했기 때문에, thread join이나 thread lock 같은 것은 사용하지 않았습니다6. 그래도 뭔가 프로그램을 정상적으로 종료하는 코드가 있었으면 좋겠다고 생각한 시간이 없는 상태의 저는 단순히 프로그램 종료 플래그를 받으면 각 세부 루틴은 loop를 끝내고 정리하는 코드를 작성해 두기는 했습니다7.

또한, 느린 쓰레드와 빠른 쓰레드가 동시에 돌면 타이밍이 잘 안 맞는 문제가 있었던 것을 해결하기 위해 빠른 쓰레드에 time sleep을 걸어주는 일을 했었습니다. 이는 당시 제가 쓰레드에 대한 정확한 이해 없이 사용하다 직면한 문제였습니다. 처음으로 전체 쓰레드를 한 번에 돌려본 날 밤에 발견했는데, 팀장님이 곰곰 생각하시더니 다음날 아침 time sleep으로 홀연히 해결했다는 일화가 있습니다.

Subroutine Multithreading 구현 특징 (subroutine.py, main.py)
  1. 여러 쓰레드의 연산 처리 시간이 차이가 나는 경우 I/O 등을 포함한 느린 쓰레드가 더 많이 느려지는 문제를 해결하기 위해 빠른 쓰레드에 time.sleep() 코드를 작성했습니다. (0.01초에서 0.03초 정도)
  2. 단순히 프로그램 종료 플래그를 받으면 각 세부 루틴의 loop를 정지하도록만 구현했습니다. thread join, lock 등을 사용한 세심한 설계를 하지 않았습니다.

아래는 쓰레드로 실행되는 각 세부 루틴이 하는 일입니다. 차량 통신과 모니터링 시스템을 제외하고는 제가 아닌 다른 팀원분들이 각자 맡아서 개발한 부분이므로 전부 자세하게 설명할 수 없어서 간략히만 설명하도록 하겠습니다.

차량 통신 Car Platform Communication

car_platform.py 에 구현되어 있습니다. 차량 통신은 시리얼(RS-232)을 이용합니다. 프로그램이 실행되고 나면 receive 와 send를 반복하며, receive 시에는 플랫폼의 속도, 조향, 엔코더 값 등의 현재 상태를 받아오고, send 시에는 제어 루틴이 내놓은 write packet을 차량 플랫폼에 보냅니다.

표지판 Sign Cam

sign_cam.py 입니다. 표지판을 보는 알고리즘입니다. Mid webcam의 이미지를 YOLO 모델에 넣어 인식한 결과를 적절히 처리한 뒤 공유 데이터 공간에 업데이트합니다. 처리하는 과정은 오인식을 줄이는 데에 그 목적이 있습니다8.

표지판 인식 개발에 관한 내용은 글을 하나 따로 써도 될 정도로 많은 일들이 있었기에(…) 이 글에서는 정말 간략히만 설명하였습니다. 이 부분에 대한 글을 한 팀원 분이 써 주셔서 링크합니다. 잡다한 개발 로그 - YOLO v3으로 표지판 인식하기

YOLO는 이미지 디텍션 딥러닝 모델입니다. (관련 글 1) (관련 글 2)

판단 Planner

planner.py 에 구현되어 있습니다. Left, right webcam의 이미지를 lane_cam.py 에서 처리한 차선 데이터와 LiDAR 센서의 값을 받아와 처리한 장애물 데이터 두 가지를 이용해 차량이 주행해야 할 경로를 생성합니다 (default). 대회 특성상 항상 표지판이 나온 후 미션이 있다는 점을 이용해 특정 표지판을 본 이후의 차량의 행동도 계획합니다 (mode).

판단 루틴에서 생성한 경로를 차량이 따라가는 데에 필요한 값을 튜플 형태의 패킷으로 만들어 공유 데이터 공간에 업데이트합니다. 제어 루틴은 이 값에 접근할 수 있습니다. 판단이 제어에게 주는 값에 대한 정의는 각 세부 루틴의 개발자 두 사람이 함께 약속한 뒤 개발이 진행되었습니다.

제어 Controller

control.py 가 해당 코드입니다. 적절한 제어 값을 계산해 통신에 넘겨줄 write packet을 만들어 공유 데이터 공간에 올려 차량 통신 루틴이 접근하도록 합니다. 대회 특성상 유턴이나 주차와 같이 매크로 (센서 인풋과 무관하게 차량의 움직임 자체를 명령 집합으로 만든 것) 를 작성해서 작동하는 부분도 여기에 구현되어 있습니다.

모니터링 시스템 Monitoring System

monitoring.py 입니다. 공유 데이터 공간에 올라오는 값들 중 사람이 실시간으로 보면 디버깅하기에 편리한 값들을 띄워줍니다. 모니터링 화면 전체를 영상 파일로 녹화하는 기능도 구현되어 있습니다.

thinkingo-2018-11

모니터링 하는 값

결론

왜 4등했는가?

이 긴 글 여기까지 읽어 주셔서 감사합니다. 읽어보시면 뭔가 그럴듯하게 잘 만든 것 같은데 왜 꼴찌를 했는지 궁금하신 분들이 분명 있으실 것 같습니다. 그래서 준비했습니다. 이 슬라이드는 그 비하인드 스토리가 적혀 있는 저의 후기 발표 슬라이드입니다…

아쉬움

막상 끝나고 찬찬히 뜯어보니 참 완성도 낮은 프로그램이었고, 디버깅은 침착하지 못했고, 여러 모로 아쉬움이 많이 남는 프로젝트였습니다. 대회 공문이 너무 늦게 나왔던 탓일까요? 3주가 길다면 길 수도 있는 시간이었는데, 5월 대회 준비할 때랑 다르게 방학 중 시간을 사용하지 못하고 학기 중에 시간을 쪼개가면서 개발한 것이라 더욱 촉박하게 느껴졌던 것 같습니다. 그럼 다른 팀들은 어떻게 한 거지? 교수님이나 대학원생 선배들에게 좀 더 많이 여쭤보고 개발 시작할 걸 하는 아쉬움도 있습니다. 학부생들끼리 뭘 알고 만들겠습니까… 공부좀 더 할걸… 긍정적으로 생각하면, 앞으로 발전할 가능성이 많은 프로젝트라고 할 수 있겠습니다. 눈물 없이는 볼 수 없는 성균관대학교 주행 영상 03:03:24 부터

팀원들

이번 팀원은 7명이었습니다. 그래도 이 대회에서 얻은 게 있다면 우리 팀원들이라고 할 수 있겠습니다. 공강 시간마다 작업실 나와서 개발하고 시간 없을 땐 짜장면 시켜먹으면서 밤새 코딩하고, 본선 대회 전날 야외 패독 천막에서 해 지고 찬바람 맞으며 회의한 것들은 참 지금 생각해도 눈물겹네요… 저희가 쓰던 작업실도 하필 2학기 때는 교내 스마트카 대회 관련 행사같은 거랑 겹쳐서 사용 못 하는 때가 많았습니다. 그럴 땐 팀원들이 계단실에서 코딩하시고 고생을 해서, 환경의 개선이 있었으면 하는 작은 바람입니다. 저는 이제 이 팀을 떠나겠지만 떠날 겁니다 앞으로 하실 분들은 인적 물적 심적으로 지원 좀 더 받았으면 좋겠습니다.

Project Repository

현재는 팀원들이 모두 동의하여 레포를 MIT 라이센스로 공개해 둔 상태입니다. 참고해 주세요. thinkingo at github


  1. 총 4팀 중 4등입니다. 

  2. 저희 학교는 몇 년 전에 파이썬을 1학년 때 필수 수강하도록 바뀌었으며 팀원의 절반 가량은 2학년이었습니다. 라고 썼지만 그냥 5월 대회 때랑 똑같이 한 겁니다. 

  3. 물론 5월 대회 때도 이 이름이었습니다. 

  4. logging, 저장 

  5. stream, 데이터 흐름 

  6. 당시 제가 threading 에 대해 잘 알지 못한 이유가 큽니다. (2018-12-10 공부 자료 추가: [Python] Tip - 스레드에서 데이터 경쟁을 막으려면 Lock을 사용

  7. 그러나 실제로 실행시켜보면 어떨 땐 잘 안 꺼지는 버그가 있습니다… 

  8. 표지판 인식 개발에 관한 내용은 글을 하나 따로 써도 될 정도로 많은 일들이 있었기에(…) 이 글에서는 정말 간략히만 설명하였습니다. 이 부분에 대한 글을 팀원 중 누군가가 써 주신다면 링크해 드리도록 하지요. (2018-12-10 링크 추가) 

  9. 주차 미션이 A or B 표지판 인식 후 해당 장소에 주차하는 것이었습니다. 

  10. 대회 끝나고야 이 값 환산에 버그가 있다는 것을 알아서 고쳤습니다…