[ 2022.10 인공지능 프로젝트 ]
라즈베리파이 OpenCV를 사용한MQTT 통신 기반 사용자 추적 쇼핑카트
사용 기술
- 라이브러리
MQTT 통신: Paho, Mosquitto
OpenCV: OpenCV (Open Source Computer Vision Library)
YOLOv4-Tiny (DeepSort 알고리즘)
- 언어
Python
- 개발 환경 및 도구
PyCharm(IDE)
Thonny
- 하드웨어 플랫폼 (IoT)
Raspberry Pi
이 외에 부수적인 것들이 많은데 일단 이 정도면 구현은 할 수 있다.
프로젝트 개요
Smart Cart(스마트카트) 라는 사물인터넷을 만드려고 한다. 스마트카트의 주기능으로는 카메라 센서, 사용자 객체 인식, 무선 통신, 센서 인식, 모터 제어가 있다. RaspberryPi의 카메라 센서를 통해 전달받은 정보를 처리하여 사용자를 인식하여 사용자의 위치정보/센서 정보를 통해 모터를 제어하는 방법으로 생각하였다.이 기능들을 다 조합하여 완성도 높은 카트를 구현하는 것이 프로젝트의 최종 목표이다.
프로젝트 전 생각
인공지능 프로젝트를 시작해보고 싶었다. 여러 기술이 들어가야 하기에 좋은 경험이 될 것 같다고 생각했고, 현재까지도 완전 상용화 되지 않은 기술이기도 하며 마트 안전사고는 카트 관련 사고가 가장 높았기 때문이다. 하지만 상용화 되지 않았다는 부분이 장점이자, 단점으로 작용할 것이라고 생각했다. 세계적인 기업들이 "고도화된 IT기술을 갖춰도 완성되지는 않았구나.." 라는 생각이 컸다. 하지만, 상용화의 목적보다는 인공지능 프로젝트 경험을 갖추기 위해 시작된 프로젝트라 일단 시작해보기로 했다.
환경 설정
개발에 필요한 환경 설정부터 진행했다(처음 사용하는 기술이다 보니, 이 부분이 너무 힘듦).
1. PyCharm에 YoloV4 설치 방법
https://www.youtube.com/watch?v=Js9kyWzKFLQ
일단 Pycharm IDE에 OpenCV와 YOLOv4-Tiny를 설치해야 한다. 초기 환경 설정은 혼자 하기에 살짝 번거로우니 위 영상을 참고하면 쉽게 할 수 있을 것이다.
Yolo란?
실시간 객체 탐지 – YOLO Real time object dectection(실시간 객체 탐지)에 사용되는 알고리즘인 YOLO를 사용 하여 실시간으로 객체 탐지를 하였다. YOLO란 You Only Look Once의 약자로, 다른 모델들에 비해 빠른 처리속도를 보여 실시간으로 객체탐지가 가능하다. SSD와 같이 하 나의 이미지 데이터를 여러개의 이미지 데이터로 나누어 분석하는 것이 아닌, 전체의 이미지를 이용해 학습하고 예측하기 때문이다.
위 그림처럼 학습된 객체를 인식하여 bounding box를 생성하여 출력한다. 이 기술을 활용하여 USER 객체를 인식하고, 그 USER를 따라오게 만들 것이다.
MQTT 프로토콜 라이브러리 설치
MQTT 기반의 메시지 송수신 프로토콜 이며, 작은 코드 공간이 필요하거나 네트워크 대역폭이 제한되는 원격 통신을 위해 사물인터넷(Internet of Thing)과 같은 제한된, 혹은 대규모 트래픽 전송을 위해 만들어 진 프로토콜이다. MQTT 통신의 동작은 메시지를 해당 Topic에 발행하고 해당 Topic에 구독하는 방식으 로 이루어진다. 이 프로토콜을 사용하는 이유는 아래 나온다.
2. RaspberryPi - Thonny
아마 기본으로 설치되어 있을 것이다. 그게 아니라면 터미널 창에서 업데이트 해보자.
sudo apt update && sudo apt upgrade -y
3. 필요한 센서 및 모터
이 부분이 가장 힘들었고, 하드웨어에 대한 지식이 없다보니 쓸데없는 곳에 돈을 지출했다. 내가 필요한 하드웨어는 Raspberry Pi에서 초음파, 모터, 부저, 카메라 센서였다. (기획할 때 필요한 것을 잘 체크해두고 돈 낭비하는 일이 없도록 해야겠다는 생각이 들었음.)
4. 카트 본체 구성
카트 본체라고 하면 RaspberryPi의 보드와 센서들이 접착된 완성체를 말하는데, 이 부분도 하드웨어이기 때문에 완성도는 기대하지 못 했다. 3D 프린터로 소형 카트의 모형을 직접 개발하였다. 자세한 내용 및 완성본은 아래에 첨부했다.
모터드라이버와 메인보드 회로도
위 사진은 메인보드와 모터드라이버 그리고 모터의 회로도인데, 모터드라이버는 모터의 제어를 위해 추가 전력이 필요하다. 모델은 L298N을 사용하였고, 9V 전지로 작동시켰다.
프로젝트 재설계
1. 난관
프로젝트 중 난관이 생겼다. 내가 생각했던 작동 방식은 라즈베리파이 자체에 YoloV4와 OpenCV를 설치하여 라즈베리파이 내에서 사용자인식/모터 및 센서 제어를 모두 할 생각이었다. 하지만 라즈베리파이는 PC보다 훨씬 가벼운 성능이고, 용량도 적기 때문에 너무 느리고 과부하가 걸렸다. 심지어 YoloV4는 GPU가 있어야 빠른 프레임으로 인식되며, CPU로만 처리한다면 너무너무 느린 속도였다.
2. 해결방안
- 속도 문제
느린 속도의 문제부터 해결하려고 했다. 스마트카트는 CCTV와 같은 고성능 카메라를 요구하지 않는다. 가까운 거리에서 사용자를 따라가기 때문에 한 객체의 인식만 필요로 한다. 그렇게 요구사항을 바꿔서 찾다보니 YoloV4의 가벼운 버전인 YoloV4-Tiny가 있었다. GPU가 없더라도 CPU로만 처리가 가능하며 속도 또한 엄청 향상되었다. (프레임은 높지만 성능은 YoloV4보다 낮음)
- 제어 문제 (통신)
위 난관처럼 라즈베리파이 자체에 YoloV4와 OpenCV를 설치를 하려고 했지만 너무 용량이 크고 무거웠다. 그래서 아예 라즈베리파이와 PC 환경이 1대1로 통신을 하여 정보를 주고 받는 것을 생각했고, 사용 기술은 MQTT 통신 프로토콜이다.
구현 동작 방식은 아래 그림과 같다.
1. MQTT 브로커와 연결하여 라즈베리파이 카메라센서에서 받은 프레임을 수신
2. TensorFlow Lite를 사용하여 YOLOv4 모델을 로드하고 추론을 수행
3. Deep SORT를 사용하여 객체 추적을 수행하고, 객체의 ID와 위치를 추적
4. 웹캠에서 받은 비디오 프레임을 처리하고, YOLOv4를 사용하여 객체를 감지한 다음, Deep SORT를 사용하여 객체를 추적하고, 추적된 객체의 위치를 시각화하여 화면에 표시
5. 객체의 위치에 따라 MQTT를 통해 해당 정보를 전송하고, 전달받은 라즈베리파이는 모터/센서를 제어
코드 구현
전체 코드를 올리기엔 너무 많기 때문에 run_webcam_yolov4_DeepSORT.py 에서의 추가적인 코드만 올렸다.
PyCharm (YoloV4-Tiny / OpenCV)
1. MQTT 통신 프로토콜 지정
MQTT_BROKER = "브로커 IP"
MQTT_RECEIVE = "파일 위치"
frame = np.zeros((240, 320, 3), np.uint8)
def on_connect(client, userdata, flags, rc):
print("Connected with result code "+str(rc))
client.subscribe(MQTT_RECEIVE)
def on_message(client, userdata, msg):
global frame
img = base64.b64decode(msg.payload)
npimg = np.frombuffer(img, dtype=np.uint8)
frame = cv2.imdecode(npimg, 1)
MQTT에서 필요한 초기 설정들을 해주고 사용할 메서드를 지정해준다. on_connect는 구독(연결)할 때 필요한 메서드이고, on_message는 카메라센서에서 프레임을 받아올 때 필요한 메서드이다.
def main(_argv):
.......
client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message
client.connect(MQTT_BROKER)
client.loop_start()
.......
위 메서드를 활용하여 라즈베리파이와 연결하는 코드를 추가하였다. 라즈베리파이에서도 잘 지정해준다면 문제없이 연결된다.
2. USER / NOT USER 확인
YoloV4-Tiny DeepSort알고리즘을 사용했는데 DeepSort알고리즘은 추론된 객체가 사람/사물일 때, 각각의 순서 ID가 붙는다. 예를 들어, Person객체라면 Person-1, Person-2, Person-3 ... 같은 방식으로 ID가 부여되는데 그 ID값을 활용하여 첫번째로 인식된 값을 무조건 USER라고 인식하여 저장했다.
if track.track_id == 1:
color = colors[int(track.track_id) % len(colors)] # 박스 색깔 지정
color = [0]
MY_User = [str(track.track_id), "USER", int(bbox[0]), int(bbox[1]), int(bbox[2]), int(bbox[3])]
# 추적된 객체 id , USER로 지정(원래는 class_name) , 좌표x, 좌표y, 좌표x+w, 좌표y+h
# bbox는 bounding box -> 객체 감싸는 박스
# (x,y,x+w,y+h) = (좌측 최상단 두개 xy 와 우측 최하단 xy)
cv2.rectangle(frame, (int(bbox[0]), int(bbox[1])), (int(bbox[2]), int(bbox[3])), color, 2)
cv2.rectangle(frame, (int(bbox[0]), int(bbox[1] - 30)),
(int(bbox[0]) + (len(class_name) + len(str(track.track_id))) * 17, int(bbox[1])), color,
-1)
cv2.putText(frame, "USER", (int(bbox[0]), int(bbox[1] - 10)), 0, 0.75, (255, 255, 255), 2)
user_w_center = (int(bbox[0]) + int(bbox[2])) // # 박스 x좌표 중앙
# user_h_center=(int(bbox[1])+int(bbox[3]))// # 박스 y좌표 중앙
# "중앙 x",user_w_center
# "중앙 y", user_h_center
if user_w_center >= 0 and user_w_center <= 80:
client.publish("smart", "left")
elif user_w_center > 80 and user_w_center <= 240:
client.publish("smart", "m_left")
elif user_w_center>240 and user_w_center<400:
client.publish("smart", "center")
elif user_w_center>=400 and user_w_center<560:
client.publish("smart", "m_right")
else:
client.publish("smart", "right")
else:
color = colors[int(track.track_id) % len(colors)]
color = [200]
cv2.rectangle(frame, (int(bbox[0]), int(bbox[1])), (int(bbox[2]), int(bbox[3])), color, 2)
cv2.rectangle(frame, (int(bbox[0]), int(bbox[1] - 30)),
(int(bbox[0]) + (len(class_name) + len(str(track.track_id))) * 17, int(bbox[1])), color,
-1)
cv2.putText(frame, "NOT USER" , (int(bbox[0]), int(bbox[1] - 10)), 0, 0.75,
(255, 255, 255), 2)
3. USER 객체 위치 정보
위 코드로만 보면 이해가 안 될 수도 있다. YoloV4에서 추적된 객체는 boundingBox를 그린다고 했다. 사용자 객체는 USER로 설정하면, USER의 바운딩 박스의 좌표 값을 알아야 사용자의 위치를 알 수가 있다.
x 좌표 값 | int(bbox[0]) |
y 좌표 값 | int(bbox[1]) |
x + w 좌표 값 | int(bbox[2]) |
y + h 좌표 값 | int(bbox[3]) |
일단 기본 창의 크기는 640x480으로 설정해뒀다. 즉, 화면의 중앙값은 x좌표 320, y좌표 240
사용자의 위치를 알아보려면 객체가 왼쪽으로 가면 x좌표 < 320, 오른쪽으로 가면 x좌표 > 320이기에 위와 같이 코드를 구현했다.
USER 객체가 왼쪽일 때는 left / m_left 로 나눴으며, 오른쪽일 때는 right / m_right로 나눠서 구분했다.
위 코드에서 client.publish("smart", "left")의 의미는 "smart"라는 구독채널에 "left"라는 메시지를 보내주는 것이다.
물론 구독/발행된 채널이름이 같아야 한다.
사용자 객체의 중앙값을 맞춰야 하기 때문에 (화면에서 밖으로 벗어나지 않고, 사용자를 따라가게 하기 위함) 바운딩 박스의 중앙값을 계산하여 정보를 전달한다.
4. USER 객체 화면 밖일 때
for track in tracker.tracks:
if not track.is_confirmed() or track.time_since_update > 1:
if track.track_id==1:
client.publish("smart", "not_track")
continue
bbox = track.to_tlbr()
class_name = track.get_class()
객체 인식 중, USER가 나가거나 인식이 되지 않을 때는 MQTT 통신을 통해 라즈베리파이에게 not_track이라는 메시지를 전송한다.
USER 인식화면
첫 객체는 USER 바운딩 박스를 그리며 인식하고, 그 후의 객체는 모두 NOT USER로 인식한다. USER의 실시간 정보를 라즈베리파이에게 MQTT 통신 프로토콜을 사용하여 전달한다.
Raspberry Pi
1. MQTT 통신 프로토콜 지정
MQTT_BROKER = "통신IP" #### pc - raspberry 맞춰줘야 함.
MQTT_SEND = "해당 주소" #### pc - raspberry 맞춰줘야 함.
client = mqtt.Client()
client.connect(MQTT_BROKER)
cap = cv.VideoCapture(0)
data = 0
def on_message(client, userdata, message
global data
data = str(message.payload.decode("utf-8"))
BROKER와 SEND는 PC환경과 맞춰주면 된다. 여기서 중요한 것은 data를 전역변수로 설정하여 프로그램 동작 중 USER가 나가더라도 0으로 초기화하여 계속 반복되는 행위를 막는 것이다.
이게 무슨 말이냐면?
while문을 도는 프로그램이 On이라는 값을 받았는데, 갑자기 통신이 끊기거나 값을 받아오지 못 할 때는
On의 값을 반복하는 것이 아니라 default 값을 받아야 한다. 그러기 위해 data를 전역변수 0으로 설정했다.
2. 카메라 프레임 전송
while True:
GPIO.output(4,False)
_, frame = cap.read()
_, buffer = cv.imencode('.jpg', frame)
jpg_as_text = base64.b64encode(buffer)
client.publish(MQTT_SEND, jpg_as_text)
client.subscribe("smart")
client.on_message = on_message
연결이 됐다면, smart를 구독하고 프레임을 인코드해서 보낸다. 그리고 실시간으로 데이터를 받는 역할을 진행한다.
이 부분만 잘 연결됐다면 전달받은 데이터를 가지고 모터/센서 등을 제어하면 된다.
하드웨어 구성
예산 문제로 인해 마트에서 사용되는 카트처럼 큰 카트를 제작하진 못 했지만, 크기 외엔 기능은 다 들어간 상태이다. 3D프린터로 2개의 기판을 제작하여 3층으로 나누어 설계했다. 참고로 내가 직접 설계하진 않았고, 여러 도움을 받았다. 각 층에 대해 기능과 구조를 설명해보자면
1층: 모터와 바퀴
2층: 라즈베리파이 보드, 모터 드라이버
3층: 카메라센서(웹캠), 초음파 센서(앞/뒤), 부저 센서, 보조배터리(무선으로 가동)
포스팅이 너무 길어질 것 같아서 하드웨어 부분을 올리지 않으려고 한다...
하드웨어 부분은 지식이 없기도 하고, 3D 프린터로 만들었기 때문에 판매처가 있는 것도 아니어서...
프로젝트 회고
하드웨어와 환경설정 시 버전의 문제로 진전이 없었던 부분이 아쉬웠으며, 처음 기획할 때의 기술에 대해 더욱 알아보고 해야될 것 같다고 생각했다. 속도와 통신의 문제를 MQTT라는 프로토콜로 해결했을 때는 정말 기분이 좋았던 것 같다. 하지만, 상용화된 기술이 아닌 이유를 알 것 같다는 생각이 들 정도로 부족한 부분이 많았다. 일단 UI가 없어서 패드나 버튼 같은 것들로 ON/OFF 제어할 수 밖에 없었다. 그리고 라즈베리파이의 센서들로는 장애물이 많고 아이들이 뛰어다니는, 변수가 많은 마트 내의 상황을 모두 인지하는 부분에서도 부족하였다. 사실 개발 비용에 비해 필요한 기능과 기술들이 너무 많이 요구되기 때문에 개발을 하지 않는 것일 거라고 생각이 들었다.
이런 소규모 프로젝트에서도 많은 비용이 지출되는데, 실제 SW/HW를 결합하여 개발하고 여러 테스트를 진행할 기업의 입장에서는 더욱 큰 부담일 것이라고 생각한다. 인공지능을 활용한 프로젝트 경험으로는 너무 값진 경험이었다.
https://github.com/min2h/SmartCart