はじめに
ラズパイの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)
TensorflowだとOpenGLESのGPU Delegateというのを使用してGPUでの計算ができるようですね。
https://qiita.com/terryky/items/fa18bd10cfead076b39f
今回はOpenCL経由でOpenCVを動かすのが最も簡単そうなので、ラズパイ3とVC4CLを使ってOpenCLでGPUを使用するという方法を取りたいと思います。
先人がいろいろとリソースを用意してくださっているため、この記事はそれのまとめと、とりあえずやってみましたという感じの記事になっています。
セットアップ
VC4CLの取得
ラズパイのGPUをOpenCLのインターフェースで使用するためのソフトです。
リポジトリのWikiに沿って、debパッケージが用意されているのでそれらをダウンロードしてインストールします。
clinfoでインストールできたかの確認を行います。(VideoCore4にアクセスするにはsudoが必要みたいです。)
sudo clinfo
OpenCVのビルド
以下のリポジトリがVC4CLを使ったOpenCVのコンパイルについてシェルスクリプトなどがまとめられています。
今回、OpenCVのバージョンは4.0.1を使っています。
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を使用します。
#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使用時の速度比較を行っていみました。
#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/
プログラムは以下のようなものを用意しました。
#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ライフを送りましょう!