ラズパイ3のGPUでOpenCVを動かす

# はじめに
ラズパイのGPUで高速化ができるという話を聞いて、これはぜひ試したいと思いラズパイを購入してOpenCV動かしてみました、という記事です。

https://qiita.com/9_ties/items/15ab7fa198991a61a3a9
https://asukiaaa.blogspot.com/2018/11/raspberry-pigpuopencl.html
https://p-0.me/b/p/593/

ラズパイのGPUを使用する方法としては、ラズパイ3であればVideoCore4というGPUが使用されており、ISA(命令セットアーキテクチャ)も公開されていてプログラムから使用するためのライブラリがいくつか公開されています。

https://github.com/doe300/VC4CL
https://github.com/nineties/py-videocore

ラズパイ4の場合はVideoCore6というGPUを使用しているのですが、IPAが公開されておらず、使用するハードルは高そうです。(しかし、ハックしてpythonから使用できるようにした強者がおられます [Link](https://xtech.nikkei.com/atcl/nxt/mag/rob/18/012600001/00062/))

TensorflowだとOpenGLESのGPU Delegateというのを使用してGPUでの計算ができるようですね。
https://qiita.com/terryky/items/fa18bd10cfead076b39f

今回はOpenCL経由でOpenCVを動かすのが最も簡単そうなので、ラズパイ3とVC4CLを使ってOpenCLでGPUを使用するという方法を取りたいと思います。
先人がいろいろとリソースを用意してくださっているため、この記事はそれのまとめと、とりあえずやってみましたという感じの記事になっています。

# セットアップ

## VC4CLの取得
ラズパイのGPUをOpenCLのインターフェースで使用するためのソフトです。
リポジトリのWikiに沿って、debパッケージが用意されているのでそれらをダウンロードしてインストールします。

https://github.com/doe300/VC4CL/wiki/How-to-get

clinfoでインストールできたかの確認を行います。(VideoCore4にアクセスするにはsudoが必要みたいです。)

```
sudo clinfo
```

## OpenCVのビルド
以下のリポジトリがVC4CLを使ったOpenCVのコンパイルについてシェルスクリプトなどがまとめられています。
今回、OpenCVのバージョンは4.0.1を使っています。

https://github.com/thortex/rpi3-opencv

aptで以下を取ってきます。

```

sudo apt-get install -y \
     build-essential \
     gettext \
     ccache \
     cmake \
     pkg-config \
     libpng-dev \
     libpng++-dev \
     libjpeg-dev \
     libtiff5-dev \
     libjasper-dev \
     libavcodec-dev \
     libavformat-dev \
     libavresample-dev \
     libswresample-dev \
     libavutil-dev \
     libswscale-dev \
     libv4l-dev \
     libxvidcore-dev \
     libx264-dev \
     libgtk-3-dev \
     libgdk-pixbuf2.0-dev \
     libpango1.0-dev \
     libcairo2-dev \
     libfontconfig1-dev \
     libatlas-base-dev \
     liblapack-dev \
     liblapacke-dev \
     libblas-dev \
     libopenblas-dev \
     gfortran \
     python-pip \
     python3-pip \
     python-numpy \
     python-dev \
     python3-dev \
     libeigen2-dev \
     libeigen3-dev \
     libopenexr-dev \
     libgstreamer1.0-dev \
     libgstreamermm-1.0-dev \
     libgoogle-glog-dev \
     libgflags-dev \
     libprotobuf-c-dev \
     libprotobuf-dev \
     protobuf-c-compiler \
     protobuf-compiler \
     libgphoto2-dev \
     qt5-default \
     libvtk6-dev \
     libvtk6-qt-dev \
     libhdf5-dev \
     freeglut3-dev \
     libgtkglext1-dev \
     libgtkglextmm-x11-1.2-dev \
     libwebp-dev \
     libtbb-dev \
     libdc1394-22-dev \
     libunicap2-dev \
     ffmpeg 
```

opencvとopencv-contribをgithubからクローンしてきて以下のようなフォルダ構成を作りました。

```
.
├── opencv
│   ├── 3rdparty
│   ├── apps
│   ├── build
│   ├── cmake
│   ├── data
│   ├── doc
│   ├── include
│   ├── modules
│   ├── platforms
│   └── samples
└── opencv_contrib
    ├── doc
    ├── modules
    └── samples
```

opencvのbuildフォルダに移動し、ビルドを行います。
cmakeオプションは上のリポジトリを参考に以下のようにしました。

```
cd opencv
mkdir build
cd build
cmake -D CMAKE_BUILD_TYPE=RELEASE \
       -D CMAKE_INSTALL_PREFIX=/usr/local \
       -D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules \
       -D BUILD_SHARED_LIBS=ON \
       -D BUILD_CUDA_STUBS=ON \
       -D BUILD_DOCS=OFF \
       -D BUILD_ZLIB=OFF \
       -D BUILD_TIFF=OFF \
       -D BUILD_JPEG=OFF \
       -D BUILD_JASPER=OFF \
       -D BUILD_PNG=OFF \
       -D BUILD_OPENEXR=OFF \
       -D BUILD_PERF_TESTS=OFF \
       -D BUILD_TBB=OFF \
       -D BUILD_WEBP=OFF \
       -D BUILD_TESTS=OFF \
       -D BUILD_EXAMPLES=OFF \
       -D BUILD_JAVA=OFF \
       -D WITH_EIGEN=ON \
       -D WITH_GSTREAMER=ON \
       -D WITH_GTK=ON \
       -D WITH_JASPER=ON \
       -D WITH_JPEG=ON \
       -D WITH_OPENEXR=ON \
       -D WITH_PNG=ON \
       -D WITH_TIFF=ON \
       -D WITH_V4L=ON \
       -D WITH_LIBV4L=ON \
       -D WITH_VTK=ON \
       -D WITH_LAPACK=ON \
       -D WITH_LAPACKE=ON \
       -D WITH_PROTOBUF=ON \
       -D WITH_1394=ON \
       -D WITH_EIGEN=ON \
       -D WITH_FFMPEG=ON \
       -D WITH_GPHOTO2=ON \
       -D WITH_OPENGL=OFF \
       -D WITH_QT=ON \
       -D WITH_TBB=ON \
       -D WITH_WEBP=ON \
       -D WITH_UNICAP=ON \
       -D WITH_OPENNI=OFF \
       -D WITH_GDAL=OFF \
       -D WITH_CUBLAS=OFF \
       -D WITH_NVCUVID=OFF \
       -D WITH_CUDA=OFF \
       -D WITH_CUFFT=OFF \
       -D WITH_IPP=OFF \
       -D WITH_IPP_A=OFF \
       -D WITH_OPENMP=ON \
       -D WITH_PTHREADS_PF=OFF \
       -D WITH_PVAPI=OFF \
       -D WITH_MATLAB=OFF \
       -D WITH_XIMEA=OFF \
       -D WITH_XINE=OFF \
       -D WITH_OPENCL=ON \
       -D WITH_OPENCLAMDBLAS=OFF \
       -D WITH_OPENCLAMDFFT=OFF \
       -D WITH_OPENCL_SVM=OFF \
       -D INSTALL_PYTHON_EXAMPLES=ON \
       -D ENABLE_CXX11=ON \
       -D ENABLE_CCACHE=ON \
       -D ENABLE_FAST_MATH=ON \
       -D ENABLE_NEON=ON \
       -D ENABLE_VFPV3=ON \
       -D ENABLE_OMIT_FRAME_POINTER=ON \
       -D BUILD_opencv_apps=ON \
       -D BUILD_opencv_aruco=ON \
       -D BUILD_opencv_bgsegm=ON \
       -D BUILD_opencv_calib3d=ON \
       -D BUILD_opencv_bioinspired=ON \
       -D BUILD_opencv_dnn=ON \
       -D BUILD_opencv_dpm=ON \
       -D BUILD_opencv_core=ON \
       -D BUILD_opencv_face=ON \
       -D BUILD_opencv_features2d=ON \
       -D BUILD_opencv_flann=ON \
       -D BUILD_opencv_freetype=ON \
       -D BUILD_opencv_fuzzy=ON \
       -D BUILD_opencv_hfs=ON \
       -D BUILD_opencv_highgui=ON \
       -D BUILD_opencv_imgcodecs=ON \
       -D BUILD_opencv_imgproc=ON \
       -D BUILD_opencv_ml=ON \
       -D BUILD_opencv_objdetect=ON \
       -D BUILD_opencv_optflow=ON \
       -D BUILD_opencv_phase_unwrapping=ON \
       -D BUILD_opencv_photo=ON \
       -D BUILD_opencv_plot=ON \
       -D BUILD_opencv_python2=ON \
       -D BUILD_opencv_python3=ON \
       -D BUILD_opencv_reg=ON \
       -D BUILD_opencv_rgbd=ON \
       -D BUILD_opencv_saliency=ON \
       -D BUILD_opencv_shape=ON \
       -D BUILD_opencv_stereo=ON \
       -D BUILD_opencv_stitching=ON \
       -D BUILD_opencv_superres=ON \
       -D BUILD_opencv_surface_matching=ON \
       -D BUILD_opencv_text=ON \
       -D BUILD_opencv_tracking=ON \
       -D BUILD_opencv_ts=ON \
       -D BUILD_opencv_video=ON \
       -D BUILD_opencv_videoio=ON \
       -D BUILD_opencv_videostab=ON \
       -D BUILD_opencv_viz=OFF \
       -D BUILD_opencv_world=OFF \
       -D BUILD_opencv_xfeature2d=ON \
       -D BUILD_opencv_ximgproc=ON \
       -D BUILD_opencv_xobjdetect=ON \
       -D BUILD_opencv_xphoto=ON \
       -D BUILD_opencv_java=OFF \
       -D BUILD_opencv_cudaarithm=OFF \
       -D BUILD_opencv_cudabgsegm=OFF \
       -D BUILD_opencv_cudacodec=OFF \
       -D BUILD_opencv_cudafeatures2d=OFF \
       -D BUILD_opencv_cudafilters=OFF \
       -D BUILD_opencv_cudaimgproc=OFF \
       -D BUILD_opencv_cudalegacy=OFF \
       -D BUILD_opencv_cudaobjdetect=OFF \
       -D BUILD_opencv_cudaoptflow=OFF \
       -D BUILD_opencv_cudastereo=OFF \
       -D BUILD_opencv_cudawarping=OFF \
       -D BUILD_opencv_cudev=OFF \
       -D Tesseract_INCLUDE_DIR=$TESS_INC_DIR \
       -D Tesseract_LIBRARY=$TESS_LIBRARY \
       -D OPENCL_INCLUDE_DIR=/usr/include \
       -D OPENCL_LIBRARY=/usr/lib/arm-linux-gnueabihf/libOpenCL.so ..
```

opencvをラズパイでビルドする際はスワップ領域を増やしておくことも忘れずに行っておきましょう。

```
swapon
sudo sed -i -e 's/SWAPSIZE=.*/SWAPSIZE=2048/g' /etc/dphys-swapfile
sudo systemctl restart dphys-swapfile.service
swapon
```

cmakeが通ったら、ビルドしてインストールします。

```
make
sudo make install
```

# 簡単なプログラムを動かしてみる

とりあえず、以下のようなカラー画像を白黒にするだけのプログラムを作り、動くかどうか試してみます。
OpenCLを使うために、UMatを使用します。

```cpp:opencv_test.cpp
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/imgcodecs.hpp>

using namespace cv;

int main(int argc, const char* argv[]) {
    Mat src = imread("lena.jpg", IMREAD_UNCHANGED);
    UMat usrc;
    src.copyTo(usrc);
    UMat dst;
    cvtColor(usrc, dst, COLOR_BGR2GRAY);
    imwrite("lena_gray.jpg", dst);
    return 0;
}
```

こちらも実行の際はsudoが必要です。

```
# ./opencv_test 
# [VC4CL] can't open /dev/mem
# [VC4CL] This program should be run as root. Try prefixing command with: sudo
sudo ./opencv_test
```

プロファイルをとって実際にGPUが使用されているか調べたかったのですが、VC4CLのプロファイラの使い方が分からなかったので、次のいろんな画像サイズでのフィルタするプログラムをCPUとGPUで速度比較して確認したいと思います。

# いろんな画像サイズでフィルタしてみる
いろんな画像サイズで`bilateralFilter`を使って、CPU使用時とGPU使用時の速度比較を行っていみました。

```cpp:filter.cpp
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>

using namespace cv;

int main(int argc, const char * argv[])
{
    /* input image */
    Mat src = imread("lena.jpg", IMREAD_UNCHANGED);
    resize(src, src, Size(), 1, 1);
    UMat usrc;
    src.copyTo(usrc);
    UMat udst, ufilt;
    cvtColor(usrc, udst, COLOR_BGR2GRAY);
    auto start = getTickCount();
    bilateralFilter(udst, ufilt, 8, 15, 15);
    auto end = getTickCount();
    double elapsedmsec = (end - start) * 1000 / getTickFrequency();
    std::cout << elapsedmsec << std::endl;
    imwrite("lena_filt.jpg", ufilt);

    return 0;
}
```

やり方としては入力画像を`resize`でサイズ変更してサイズを大きくしていった際に`bilateralFilter`にかかった時間を計測します。
CPUで実行する場合は、上のプログラムの`UMat`を普通の`Mat`にすることでCPUで実行可能です。
サイズとかかった時間の表を以下に示します。

| 画像サイズ | CPU[ms] | GPU[ms] |
|----------|---------|---------|
| 512x512  | 48.2466 | 336.359 |
| 2560x2560 | 557.283 | 7510.3 |
| 5120x5120 | 2022.19 | 2541.41 |
| 7680x7680 | 4699.34 | 5896.73 |
| 10240x10240 | 22274.4 | 18024.3 |
| 12800x12800 | 35434.4 | 32194.6 |

これ以上はメモリ不足で実行できませんでした。10240x10240から少しGPUのほうが速くなってきているという感じでした。
(なぜかGPUでは線形に計算量が増えず途中で一旦計算時間が減るという現象が起きています。)


# おまけ(Yolov3を動かしてみる→動かない)
DNNモジュールが動くと嬉しいなと思い、Yolov3の学習済みモデルを以下から取ってきて、OpenCV+VC4Lで動くかどうか試してみました。
https://pjreddie.com/darknet/yolo/

プログラムは以下のようなものを用意しました。

```cpp:yolo.cpp
#include <opencv2/dnn.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/core/utils/trace.hpp>
#include <iostream>
#include <vector>
using namespace cv;
using namespace cv::dnn;
using namespace std;

string CLASSES[] = {"person", "bicycle", "car", "motorbike", "aeroplane", "bus", "train", "truck",
                    "boat", "traffic", "light", "fire", "hydrant", "stop", "sign", "parking", "meter",
                    "bench", "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra",
                    "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis",
                    "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard",
                    "surfboard", "tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon",
                    "bowl", "banana", "apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza",
                    "donut", "cake", "chair", "sofa", "pottedplant", "bed", "diningtable", "toilet", "tvmonitor",
                    "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave", "oven", "toaster",
                    "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier",
                    "toothbrush"};


vector<String> getOutputsNames(const Net& net) {
    static vector<String> names;
    if (names.empty()) {
        vector<int> outLayers = net.getUnconnectedOutLayers();
        vector<String> layersNames = net.getLayerNames();
        names.resize(outLayers.size());
        for (size_t i = 0; i < outLayers.size(); ++i)
            names[i] = layersNames[outLayers[i] - 1];
    }
    return names;
}

void draw_box(int classId, float conf, int left, int top, int right, int bottom, Mat& frame) {
    rectangle(frame, Point(left, top), Point(right, bottom), Scalar(255, 178, 50), 3);
    string label = format("%.2f", conf);
    label = CLASSES[classId] + ":" + label;
    int baseLine;
    Size labelSize = getTextSize(label, FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
    top = max(top, labelSize.height);
    rectangle(frame, Point(left, top - round(1.5*labelSize.height)), Point(left + round(1.5*labelSize.width), top + baseLine), Scalar(255, 255, 255), FILLED);
    putText(frame, label, Point(left, top), FONT_HERSHEY_SIMPLEX, 0.75, Scalar(0,0,0), 1);
}

void postprocess(Mat& frame, const vector<Mat>& outs) {
    const float confThreshold = 0.5;
    const float nms = 0.4;
    vector<int> classIds;
    vector<float> confidences;
    vector<Rect> boxes;
    for (size_t i = 0; i < outs.size(); ++i) {
        float* data = (float*)outs[i].data;
        for (int j = 0; j < outs[i].rows; ++j, data += outs[i].cols) {
            Mat scores = outs[i].row(j).colRange(5, outs[i].cols);
            Point classIdPoint;
            double confidence;
            minMaxLoc(scores, 0, &confidence, 0, &classIdPoint);
            if (confidence > confThreshold) {
                int centerX = (int)(data[0] * frame.cols);
                int centerY = (int)(data[1] * frame.rows);
                int width = (int)(data[2] * frame.cols);
                int height = (int)(data[3] * frame.rows);
                int left = centerX - width / 2;
                int top = centerY - height / 2;
                classIds.push_back(classIdPoint.x);
                confidences.push_back((float)confidence);
                boxes.push_back(Rect(left, top, width, height));
            }
        }
    }
    vector<int> indices;
    NMSBoxes(boxes, confidences, confThreshold, nms, indices);
    for (size_t i = 0; i < indices.size(); ++i)
    {
        int idx = indices[i];
        Rect box = boxes[idx];
        draw_box(classIds[idx], confidences[idx], box.x, box.y,
                 box.x + box.width, box.y + box.height, frame);
    }
}

int main(int argc, char **argv)
{
    CV_TRACE_FUNCTION();
    String modelConfiguration = "yolov3.cfg";
    String modelWeights = "yolov3.weights";

    String imageFile = "2.jpeg";
    Net net = dnn::readNetFromDarknet(modelConfiguration, modelWeights);
    net.setPreferableBackend(DNN_BACKEND_OPENCV);
    net.setPreferableTarget(DNN_TARGET_OPENCL);
    Mat frame = imread(imageFile);
    
    Mat inputBlob = blobFromImage(frame, 1 / 255.0, Size(416, 416), Scalar(0, 0, 0), true);
    net.setInput(inputBlob);
    vector<Mat> outs;
    net.forward(outs, getOutputsNames(net));
    postprocess(frame, outs);

    vector<double> layersTimes;
    double freq = getTickFrequency() / 1000;
    double t = net.getPerfProfile(layersTimes) / freq;
    string label = format("Inference time for a frame : %.2f ms", t);
    putText(frame, label, Point(0, 15), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 0, 255));
     
    imwrite("detection.jpg", frame);
    return 0;
}

```

いくつか関連するイシューが挙がっており、VC4CLをオプション変えてコンパイルしたり、OpenCVのDNNモジュールがOpenCLではIntel向けのオプションを渡しているっぽいのでそれを無視するようにしてみたりしてみましたが、結局動きませんでした。

https://github.com/opencv/opencv/issues/13852
https://github.com/doe300/VC4CL/issues/59
https://github.com/doe300/VC4CL/issues/29

イシューでは動いているみたいなコメントもあったのですが、やり方の詳細があまり書いておらず実際どうなのか分かりません。


# 感想
GPUの効果を得るにはかなり大きな画像じゃないと厳しい感じでしたが、ラズパイのリソースを使いこなしている感があって楽しいですね。
`bilateralFilter`以外の関数でどういう結果になるかも気になります。
慣れてきたら、Realsenseとか使ってOpenCLを使った点群処理とかも行わせてみたいなと思います。
来年も良いOpenCVライフを送りましょう！