RaspberryPi3에서의 YOLOv4 사용

최근 리뷰했던 논문들에서 죄다 YOLO얘기만 한다.
뭔지는 대강 알고있지만 써본적은 없었기에 이 참에 한 번 써보고자 한다.

굴러다니는 RaspberryPi3에 YOLO를 돌려보기로 한다.
마침 YOLOv4가 나왔기에 그걸로 올려봤다.

총 설치시간은 5분이 채 안된다.

YOLOv4 설치

필자가 사용한 RaspberryPi는 안 쓴지 꽤 됐기때문에 새로 이미지를 설치해 사용했다. (Raspbian 10/Buster)

YOLOv4를 설치하기 앞서 Dependency를 설치해야 한다. (참고로 필자의 Python 버전은 3.7.3이다)
Dependency 설치는 다행히 오래걸리지 않는다.
아래의 Dependency 설치 구문은 설치 도중 오류가 발생할 수 있기 때문에 일부러 나누어두었다.
(필자는 발생하지 않았지만, 발생한다면 적당히 트러블슈팅해주자)

$ python3 -m pip install -U pip setuptools wheel numpy
$ python3 -m pip install -U opencv-python

tensorflow를 설치하기 위해서는 tensorflow wheel을 내려받아야 한다.
그렇지 않으면 RaspberryPi3 기준으로 이전 버전(1.x)의 tensorflow가 설치된다.
wheel 파일은 공식 웹사이트의 맨 아래 글에서 안내한다.
하지만 글 작성시점(20.11.04)에서 웹사이트에는 RaspberryPi3에 대해 Python3.5버전 tensorflow밖에 제공하지 않았다.

그래서 검색 끝에 Prebuilt된 tensorflow를 사용하였다.
해당 Github는 RaspberryPi를 위해 경량화된 tensorflow binary를 제공한다.
다행히 설치하는 방법도 자세하게 안내되어있어 크게 어려울 것은 없다.
해당 사이트에서 다음과 같은 방법으로 tensorflow를 설치할 것을 안내한다.
참고로 필자는 tensorflow 2.3.1 버전을 설치하였다.

$ sudo apt-get install -y libhdf5-dev libc-ares-dev libeigen3-dev gcc gfortran python-dev libgfortran5 \
                          libatlas3-base libatlas-base-dev libopenblas-dev libopenblas-base libblas-dev \
                          liblapack-dev cython libatlas-base-dev openmpi-bin libopenmpi-dev python3-dev
$ wget "https://raw.githubusercontent.com/PINTO0309/Tensorflow-bin/master/tensorflow-2.3.1-cp37-none-linux_armv7l_download.sh"
$ ./tensorflow-2.3.1-cp37-none-linux_armv7l_download.sh
$ sudo pip3 uninstall tensorflow # If installed already, remove it.
$ sudo -H pip3 install tensorflow-2.3.1-cp37-none-linux_armv7l.whl

【Required】 Restart the terminal.

이후 YOLOv4를 설치한다.

$ python3 -m pip install yolov4

전혀 어렵지 않았다.

YOLOv4 사용

똑같이 이 Github에서 샘플 이미지로부터 물체 인식하는 예제 코드를 제공한다.
먼저 Github에서 소개하는 것 처럼 아래와 같이 예제코드를 내려받자.
예제코드에서는 Mobilenet에서 물체 인식을 위한 사전 훈련된 가중치(Pre-trained Weights)들을 사용한다.
경로는 마음대로 설정하면 된다.

$ cd ~;mkdir test
$ curl https://raw.githubusercontent.com/tensorflow/tensorflow/master/tensorflow/lite/examples/label_image/testdata/grace_hopper.bmp > ~/test/grace_hopper.bmp
$ curl https://storage.googleapis.com/download.tensorflow.org/models/mobilenet_v1_1.0_224_frozen.tgz | tar xzv -C ~/test mobilenet_v1_1.0_224/labels.txt
$ mv ~/test/mobilenet_v1_1.0_224/labels.txt ~/test/
$ curl http://download.tensorflow.org/models/mobilenet_v1_2018_02_22/mobilenet_v1_1.0_224_quant.tgz | tar xzv -C ~/test

그리고 예제를 위한 label_image.py 파일은 다음과 같다.

import argparse
import numpy as np
import time

from PIL import Image

# Tensorflow -v1.12.0
#from tensorflow.contrib.lite.python import interpreter as interpreter_wrapper

# Tensorflow v1.13.0+, v2.x.x
from tensorflow.lite.python import interpreter as interpreter_wrapper

def load_labels(filename):
  my_labels = []
  input_file = open(filename, 'r')
  for l in input_file:
    my_labels.append(l.strip())
  return my_labels
if __name__ == "__main__":
  floating_model = False
  parser = argparse.ArgumentParser()
  parser.add_argument("-i", "--image", default="/tmp/grace_hopper.bmp", \
    help="image to be classified")
  parser.add_argument("-m", "--model_file", \
    default="/tmp/mobilenet_v1_1.0_224_quant.tflite", \
    help=".tflite model to be executed")
  parser.add_argument("-l", "--label_file", default="/tmp/labels.txt", \
    help="name of file containing labels")
  parser.add_argument("--input_mean", default=127.5, help="input_mean")
  parser.add_argument("--input_std", default=127.5, \
    help="input standard deviation")
  parser.add_argument("--num_threads", default=1, help="number of threads")
  args = parser.parse_args()

  ### Tensorflow -v2.2.0
  #interpreter = interpreter_wrapper.Interpreter(model_path=args.model_file)
  ### Tensorflow v2.3.0+
  interpreter = interpreter_wrapper.Interpreter(model_path=args.model_file, num_threads=int(args.num_threads))
  
  interpreter.allocate_tensors()
  input_details = interpreter.get_input_details()
  output_details = interpreter.get_output_details()
  # check the type of the input tensor
  if input_details[0]['dtype'] == np.float32:
    floating_model = True
  # NxHxWxC, H:1, W:2
  height = input_details[0]['shape'][1]
  width = input_details[0]['shape'][2]
  img = Image.open(args.image)
  img = img.resize((width, height))
  # add N dim
  input_data = np.expand_dims(img, axis=0)
  if floating_model:
    input_data = (np.float32(input_data) - args.input_mean) / args.input_std

  ### Tensorflow -v2.2.0
  #interpreter.set_num_threads(int(args.num_threads))
  interpreter.set_tensor(input_details[0]['index'], input_data)

  start_time = time.time()
  interpreter.invoke()
  stop_time = time.time()

  output_data = interpreter.get_tensor(output_details[0]['index'])
  results = np.squeeze(output_data)
  top_k = results.argsort()[-5:][::-1]
  labels = load_labels(args.label_file)
  for i in top_k:
    if floating_model:
      print('{0:08.6f}'.format(float(results[i]))+":", labels[i])
    else:
      print('{0:08.6f}'.format(float(results[i]/255.0))+":", labels[i])

  print("time: ", stop_time - start_time)

위의 예제는 쓰레드 개수를 달리 했을 때 얼마나 시간이 단축되었는지를 보여주는 코드이다.
필자의 경우에는 Github에서 소개한 대로 물체 인식이 크게 잘 되지도 않았다.
(그리고 tensorflow를 import하는데 시간이 10초정도 걸린다)
Github에서는 tensorflow 1.x 버전을 사용한 모양인데 그 때문인듯 하다.

python3 label_image.py \
> --num_threads 1 \
> --image grace_hopper.bmp \
> --model_file mobilenet_v1_1.0_224_quant.tflite \
> --label_file labels.txt
0.035294: 835:suit, suit of clothes
time:  0.33597850799560547

python3 label_image.py \
> --num_threads 4 \
> --image grace_hopper.bmp \
> --model_file mobilenet_v1_1.0_224_quant.tflite \
> --label_file labels.txt
0.035294: 835:suit, suit of clothes
time:  0.09435772895812988

작성중…

이에 tensorflow 2.x 버전을 사용하는 Mobilenet 가중치를 찾아보았다.
역시 없을리가 없었다.
필자는 배포되는 가중치들 중 SSD MobileNet v2 320×320 버전을 사용했다.
이를 사용하기 위해서는 귀찮지만 tensorflow lite로 변환을 해주어야 한다. [참고]

$ curl http://download.tensorflow.org/models/object_detection/tf2/20200711/ssd_mobilenet_v2_320x320_coco17_tpu-8.tar.gz | tar xzv -C .

여기서 tensorflow lite로 변환하는 방법을 소개한다.
필자는 다음과 같이 convert2lite.py를 만들어 사용하였다.

import sys
import tensorflow as tf

# Check arguments
if len(sys.argv) is not 2:
  print('Model directory is required as an argument.')
  exit(1)

# Convert the model
saved_model_dir = sys.argv[1]
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir) # path to the SavedModel directory
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8,
                                       tf.lite.OpsSet.TFLITE_BUILTINS]
tflite_model = converter.convert()

# Save the model
with open('model.tflite', 'wb') as f:
  f.write(tflite_model)

그 후, 다음과 같이 실행하면 tflite 파일을 얻을 수 있다.
하지만 RaspberryPi 파워로는 어림도 없는 작업이므로 반드시 PC-grade에서 작업해주어야 한다.
현재 PC-grade에서 돌려본 결과 버그인지 뭔지 모르겠지만 변환이 안되고있다. 확인중

$ python3 convert2lite.py ssd_mobilenet_v2_320x320_coco17_tpu-8/saved_model

실시간 추론 (Real-time Inference) – TBA

시간이 되면 Camera Module v2를 이용해 실시간 inference를 진행해볼 예정이다.

댓글 남기기