Raspberry PIで（そこそこ）リアルタイムにYOLOを動かしてみる (darknet-nnpack)

#やったこと
Raspberry PI3上で、カメラモジュールの画像をtiny-YOLOv2に入力し、1FPS弱ぐらいで認識結果を表示できるようにした。
![yolo.png](https://qiita-image-store.s3.amazonaws.com/0/318883/bb47bc99-99b5-fe30-044e-534bb9287ed0.png)

#背景
DeepLeaningの物体認識で有名な[YOLO](https://pjreddie.com/darknet/)。
RaspberryPiでサンプル画像を認識させているのはそこそこ見つかる。
http://yagitsawa.github.io/2018/04/07/darknet-with-nnpack/

これをカメラモジュールで連続処理できたら面白かろうと試してみたが、なかなか大変だった。

使ったハードは以下。
[Raspberry Pi3 コンプリートスターターキット](https://amzn.to/2R9K0uM)
[HiLetgo OV5647 5MP Raspberry Pi 3 カメラ OV5647 HDカメラモジュール](https://amzn.to/2RcHuDR)

#カメラモジュールからの画像取得
よくあるVideoCaptureだとなぜか遅すぎて使えなかったので、Raspicamを使う。
https://github.com/cedricve/raspicam
[別の記事](https://qiita.com/jonatechout/items/6ce60d31cbf30ffae461)にサンプルコードあり。

#darknet-nnpackのインストール、C++用に改変
背景のリンク先にあるdarknet-nnpackはGCCでコンパイルするのだが、今回はOpenCVのためC++を使いたい。
extern "C" なども試したが、どうもdarknetの中でCとC++が共存しているらしく、うまくコンパイルできなかった。
そこで、darknet-nnpackを改変してC++でコンパイルできるようにしたのがこちら。
https://github.com/jonatechout/darknet-nnpack

具体的には、classやnewといったC++の予約語を変数名に使わないようにした。
インストール方法は上記gitリンク参照。

#コード
メインのコードはこちら。
https://github.com/jonatechout/raspi_yolo_test
インストール方法はgit参照。

```
#include <darknet.h>
#include <nnpack.h>
#include <unistd.h>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <sys/time.h>
#include <raspicam/raspicam_cv.h>

using namespace cv;

void convertCvImageToDnImage(const Mat& mat, image* out_img)
{
    for(int y = 0; y < out_img->h; ++y){
      for(int x = 0; x < out_img->w; ++x){
        for(int k= 0; k < out_img->c; ++k){
          out_img->data[k * out_img->h * out_img->w + y * out_img->w + x]
            = static_cast<float>(mat.data[y * mat.step + x * mat.elemSize() + k]) / 255.0;
        }
      }
    }
}

void convertDnImageToCvImage(const image& dnetImg, Mat* out_mat)
{
  for(int y = 0; y < dnetImg.h; ++y){
    for(int x = 0; x < dnetImg.w; ++x){
      for(int k= 0; k < dnetImg.c; ++k){
        out_mat->data[y * out_mat->step + x * out_mat->elemSize() + k]
          =  static_cast<unsigned char>(dnetImg.data[k * dnetImg.h * dnetImg.w + y * dnetImg.w + x] * 255);
      }
    }
  }
}

int main(int argc, char* argv[])
{
  if (argc < 4)
  {
    printf("4 arguments are necessary.\n");
    printf("%s <data config path> <network config path> <network weights path>\n", argv[0]);
    return -1;
  }

  const unsigned int IMAGE_WIDTH = 320;
  const unsigned int IMAGE_HEIGHT = 240;

  raspicam::RaspiCam_Cv camera;
  camera.set( CV_CAP_PROP_FORMAT, CV_8UC3 );
  camera.set( CV_CAP_PROP_FRAME_WIDTH, IMAGE_WIDTH );
  camera.set( CV_CAP_PROP_FRAME_HEIGHT, IMAGE_HEIGHT );

  if (!camera.open())
  {
    printf("camera open error\n");
    return -1;
  }

  char *datacfg = argv[1];
  char *cfgfile = argv[2];
  char *weightfile = argv[3];
  float thresh = 0.24;
  float hier_thresh = 0.5;

  list *options = read_data_cfg(datacfg);
  char *name_list = option_find_str(options, const_cast<char*>("names"), const_cast<char*>("data/names.list"));
  char **names = get_labels(name_list);

  image **alphabet = load_alphabet();
  network *net = load_network(cfgfile, weightfile, 0);
  set_batch_network(net, 1);
  srand(2222222);

  float nms=.3;
  nnp_initialize();
  net->threadpool = pthreadpool_create(0);

  namedWindow("prediction", CV_WINDOW_AUTOSIZE);
  image dnetImg = make_image(IMAGE_WIDTH, IMAGE_HEIGHT, 3);

  layer l = net->layers[net->n-1];
  box *boxes = static_cast<box*>(calloc(l.w*l.h*l.n, sizeof(box)));
  float **probs = static_cast<float**>(calloc(l.w*l.h*l.n, sizeof(float *)));

  for(int j = 0; j < l.w*l.h*l.n; ++j) {
    probs[j] = static_cast<float*>(calloc(l.classes + 1, sizeof(float *)));
  }

  printf("ready\n");

  while(1){
    Mat mat;
    camera.grab();
    camera.retrieve(mat);

    convertCvImageToDnImage(mat, &dnetImg);

    image sized = letterbox_image_thread(dnetImg, net->w, net->h, net->threadpool);
    float *X = sized.data;

    struct timeval start, stop;
    gettimeofday(&start, 0);

    network_predict(net, X);

    gettimeofday(&stop, 0);
    printf("Predicted in %ld ms.\n", (stop.tv_sec * 1000 + stop.tv_usec / 1000) - (start.tv_sec * 1000 + start.tv_usec / 1000));

    get_region_boxes(l, dnetImg.w, dnetImg.h, net->w, net->h, thresh, probs, boxes, NULL, 0, 0, hier_thresh, 1);

    if (nms) do_nms_sort(boxes, probs, l.w*l.h*l.n, l.classes, nms);
    draw_detections(dnetImg, l.w*l.h*l.n, thresh, boxes, probs, NULL, names, alphabet, l.classes);

    Mat disp(dnetImg.h, dnetImg.w, CV_8UC3);
    convertDnImageToCvImage(dnetImg, &disp);
    imshow("prediction", disp);
    waitKey(1);

    free_image(sized);
  }

  free_image(dnetImg);
  free(boxes);
  free_ptrs((void **)probs, l.w*l.h*l.n);

  pthreadpool_destroy(net->threadpool);
  nnp_deinitialize();

  return 0;
}
```

最終的に以下のコマンドで開始する。

```
./raspiyolotest cfg/voc.data cfg/yolov2-tiny-voc.cfg yolov2-tiny-voc.weights 
```
結果

```
layer     filters    size              input                output
    0 conv     16  3 x 3 / 1   416 x 416 x   3   ->   416 x 416 x  16
    1 max          2 x 2 / 2   416 x 416 x  16   ->   208 x 208 x  16
    2 conv     32  3 x 3 / 1   208 x 208 x  16   ->   208 x 208 x  32
    3 max          2 x 2 / 2   208 x 208 x  32   ->   104 x 104 x  32
    4 conv     64  3 x 3 / 1   104 x 104 x  32   ->   104 x 104 x  64
    5 max          2 x 2 / 2   104 x 104 x  64   ->    52 x  52 x  64
    6 conv    128  3 x 3 / 1    52 x  52 x  64   ->    52 x  52 x 128
    7 max          2 x 2 / 2    52 x  52 x 128   ->    26 x  26 x 128
    8 conv    256  3 x 3 / 1    26 x  26 x 128   ->    26 x  26 x 256
    9 max          2 x 2 / 2    26 x  26 x 256   ->    13 x  13 x 256
   10 conv    512  3 x 3 / 1    13 x  13 x 256   ->    13 x  13 x 512
   11 max          2 x 2 / 1    13 x  13 x 512   ->    13 x  13 x 512
   12 conv   1024  3 x 3 / 1    13 x  13 x 512   ->    13 x  13 x1024
   13 conv   1024  3 x 3 / 1    13 x  13 x1024   ->    13 x  13 x1024
   14 conv    125  1 x 1 / 1    13 x  13 x1024   ->    13 x  13 x 125
   15 detection
mask_scale: Using default '1.000000'
Loading weights from yolov2-tiny-voc.weights...Done!
ready
Predicted in 1318 ms.
Predicted in 1240 ms.
tvmonitor: 36%
Predicted in 1258 ms.
tvmonitor: 45%
Predicted in 1263 ms.
bottle: 29%
Predicted in 1260 ms.
tvmonitor: 37%
Predicted in 1245 ms.
tvmonitor: 32%
Predicted in 1257 ms.
tvmonitor: 43%
Predicted in 1258 ms.
tvmonitor: 53%
Predicted in 1272 ms.
Predicted in 1301 ms.
person: 55%
person: 27%
Predicted in 1270 ms.
person: 82%
person: 30%
```

1フレーム1.3秒ほど。リアルタイムと言っていいかわからないが、それなりに使い道はある気がする。
認識結果を外部で扱いたい場合は、boxesとprobsを参照すればいいと思う。