OpenCV 4.0のQRコード検出、G-APIを試す

先日、OpenCV 4.0が正式リリースされました。リリースノートの内容を載せます。

* OpenCV is now C++11 library and requires C++11-compliant compiler. Minimum required CMake version has been raised to 3.5.1.
* A lot of C API from OpenCV 1.x has been removed.
* Persistence (storing and loading structured data to/from XML, YAML or JSON) in the core module has been completely reimplemented in C++ and lost the C API as well.
* New module G-API has been added, it acts as an engine for very efficient graph-based image procesing pipelines.
* dnn module was updated with Deep Learning Deployment Toolkit from the OpenVINO™ toolkit R4. See the guide how to build and use OpenCV with DLDT support.
* dnn module now includes experimental Vulkan backend and supports networks in ONNX format.
* The popular Kinect Fusion algorithm has been implemented and optimized for CPU and GPU (OpenCL)
* QR code detector and decoder have been added to the objdetect module
* Very efficient and yet high-quality DIS dense optical flow algorithm has been moved from opencv_contrib to the video module.

まずは概要を確認すると、OpenCV4.0では実行環境がモダンになっていて(とはいえそれほど新しくはないのですが)、多くのC言語のインタフェースがついに削除されたようです。下位互換は保証されてないので業務で利用する際には注意が必要そうです。機能面ではGraph API (G-API)やDNNモジュールのIntel Deep Learning Deployment Toolkit (DLDT)連携、ONNXフォーマット対応など、近年のエッジデバイスにおける機械学習ブームを意識しているように見えますね。また、非機能面においても多くの性能改善が施されているようです。Intel CPUならAVX2命令をより活用するようになったりと、幅広い環境で高いパフォーマンスを発揮できるような細かな改善が見られます。さすが、メジャーバージョンが上がっただけのことはあります。

環境

* CentOS 7.5 (x86_64/Xeon 8core CPU/16GB RAM)
* GCC 4.8.5
* CMake 3.12.2
* Python 3.6.5 (Anaconda custom)

いつものようにLinuxサーバ環境上に構築しました。今回はGPU(CUDA)環境は無しで試します。

インストール

OpenCV3.x系と比べて環境に対する要求がいくらか上がっているようなので注意してください。特にC++11以降が必須なのが大きいかなと思います。まずは今回の環境で必要なパッケージ群を予めインストールしておきます。Python関連はAnaconda環境を構築した段階で揃うはずなのでここでは割愛。また、Linuxサーバ環境にインストールするので各種GUI環境も利用しません。

$ sudo yum install gcc-c++ openblas-devel lapack-devel eigen3-devel tbb-devel

*-develパッケージの方を入れると依存パッケージも大体一緒に入れてくれるはず。UbuntuとかのDebian系なら*-devパッケージでOK。

# OpenCVソースコードの取得
$ wget https://github.com/opencv/opencv/archive/4.0.0.zip
$ unzip 4.0.0.zip
$ cd opencv-4.0.0
$ mkdir build; cd $_
# CMake実行 ($PYENV_ROOTはpyenvのインストールディレクトリ)
$ PYENV_VERSION=$PYENV_ROOT/versions/anaconda3-5.2.0
$ cmake3 \
    -D CMAKE_INSTALL_PREFIX=/usr/local/opencv \
    -D PYTHON3_EXECUTABLE=$PYENV_ROOT/shims/python3 \
    -D PYTHON3_INCLUDE_DIR=$PYENV_VERSION/include/python3.6m \
    -D PYTHON3_LIBRARY=$PYENV_VERSION/lib \
    -D PYTHON3_PACKAGES_PATH=$PYENV_VERSION/lib/python3.6/site-packages \
    -D PYTHON3_NUMPY_INCLUDE_DIRS=$PYENV_VERSION/lib/python3.6/site-packages/numpy/core/include \
    -D WITH_TBB=1 \
    ..
$ make -j
$ sudo make install

僕の場合はpyenv環境上でPythonでの開発を普段行なっているので、OpenCVのPythonインタフェースもpyenv環境に適用します。それ以外だとIntel TBB利用を明示的に指定しているくらいです。各画像コーデック/デコーダ、Intel IPP や Protocol Buffer 等のサードパーティライブラリはCMake側でインストールチェック、デフォルトで有効化してくれますので、ごちゃごちゃとオプションを細かく指定する煩雑さは軽減されているようです。GPU(CUDA)環境を利用する場合はもう少し設定が増えて複雑になりますけど今回は利用しませんので。

ただし、2018/12現在、環境によってはLAPACKのリンクをオフにする必要があるようです。CMakeオプション WITH_LAPACK=0 にすれば良いです。GitHub issueにも挙がっています。

動作確認

せっかくなのでOpenCV4.0から追加された新機能を試してみます。目玉機能の一つとして注目度の高いG-APIを触る前に、まずは気軽に使えそうなQRコード検出機能を試してみました。

QRコード検出

入力画像として以下のQRコード画像を使います。このブログのURL(https://rest-term.com)情報を埋め込んで作りました。

まずはC++インタフェースから試してみます。

* qrcode_sample.cpp

#include 
#include "opencv2/objdetect.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"

int main(int argc, char **argv) {
    using namespace std;

    const string file_name = "qrcode_sample.png";
    const cv::Mat input_image = cv::imread(file_name, cv::IMREAD_COLOR);
    cv::Mat output_image = input_image.clone();
    vector points;
    cv::Mat straight_qrcode;
    // QRコード検出器
    cv::QRCodeDetector detector;
    // QRコードの検出と復号化(デコード)
    const string data = detector.detectAndDecode(input_image, points, straight_qrcode);
    if(data.length() > 0) {
        // 復号化情報(文字列)の出力
        cout << "decoded data: " << data << endl;
        // 検出結果の矩形描画
        for(size_t i = 0; i < points.size(); ++i) {
            cv::line(output_image, points[i], points[(i + 1) % points.size()], cv::Scalar(0, 0, 255), 4);
        }
        cv::imwrite("output.png", output_image);
        // おまけでQRコードのバージョンも計算
        cout << "QR code version: " << ((straight_qrcode.rows - 21) / 4) + 1 << endl;
    } else {
        cout << "QR code not detected" << endl;
    }
    return 0;
}

OpenCVのAPIは名前空間を省略せずに書いたので、cv::が付いている箇所を中心に見てみると少ないコードでQRコード検出できることがわかります。検出器のインスタンスを作って入力画像や出力結果を入れるコンテナを渡すだけです。C++11以降で使える型推論等の機構は使ってませんが、この規模のコードだとそれほど効果的ではないと思います。

* CMakeLists.txt

cmake_minimum_required(VERSION 3.5.1)
project(qrcode_sample)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_FLAGS "-Wall -g")
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})
add_executable(qrcode_sample qrcode_sample.cpp)
target_link_libraries(qrcode_sample ${OpenCV_LIBRARIES})

CMakeの最小バージョンは一応OpenCV4.0本体と合わせておきました。あとはC++11以降でコンパイルすることに注意しておけば大丈夫そうです。

* 実行結果

$ cmake3 .
-- The C compiler identification is GNU 4.8.5
-- The CXX compiler identification is GNU 4.8.5
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found OpenCV: /usr/local/opencv (found version "4.0.0") 
-- Configuring done
-- Generating done
-- Build files have been written to: /home/ryo/workspace/dev/opencv-samples/objdetect
$ make
Scanning dependencies of target qrcode_sample
[ 50%] Building CXX object CMakeFiles/qrcode_sample.dir/qrcode_sample.cpp.o
[100%] Linking CXX executable qrcode_sample
[100%] Built target qrcode_sample
$ ./qrcode_sample
decoded data: https://rest-term.com
QR code version: 3

* 出力画像

正しくQRコードを検出できていますし、URL情報のデコードにも成功しています。QR決済のシステムやアプリに組み込むならデコード処理だけで良さそうです。また、QRコードの規格にはバージョンというものがありまして、QRコードの大きさはバージョン1の21×21セルからバージョン40の177×177セルまで4セル刻みで決められています。cv::QRCodeDetector::detectAndDecodeの3つめの引数にバージョンに合わせた解像度(セル数)でQRコードを書き出してくれるのでバージョン番号も計算できます。今回作成したQRコードだとバージョン3(29x29セル)だということがわかります。

次はPythonから使ってみます。PYTHONPATHは予め通しておきます。

* qrcode_sample.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import cv2

if __name__ == '__main__':
    file_name = 'qrcode_sample.png'
    input_image = cv2.imread(file_name)
    output_image = input_image.copy()
    detector = cv2.QRCodeDetector()
    data, points, straight_qrcode = detector.detectAndDecode(input_image)
    if data:
        print(f'decoded data: {data}')
        for i in range(4):
            cv2.line(output_image, tuple(points[i][0]), tuple(points[(i + 1) % len(points)][0]), (0, 0, 255), 4)
        cv2.imwrite('output.png', output_image)
        print(f'QR code version: {((straight_qrcode.shape[0] - 21) / 4) + 1}')

* 実行結果 (出力画像はC++版と同じなので省略)

$ chmod +x qrcode_sample.py
$ ./qrcode_sample.py
decoded data: https://rest-term.com
QR code version: 3.0

Pythonインタフェースも動作に問題なさそうです。パッケージが cv2 なのはそろそろ変えて欲しいところですけども。

Graph API (G-API)

次はG-APIを試してみます。一連の処理を計算グラフとして構築・適用する機構となっています。DNNフレームワークだと一般的に備わっている仕組みで、Web系バックエンドでもよく利用されるApache Sparkのような分散処理環境においても同様の考え方で処理を記述することができます。膨大なロジックであってもグラフ上での局所的な計算の繋がり(組み合わせ)として考えるので、途中計算結果を共有できる(システム的にはキャッシュとして扱える)のはスループット向上に繋がりそうです。

公式のG-APIのチュートリアルはビデオ入力を伴っているので、ここでは少し修正して入力・出力共に静止画像を扱うようにしました。

* 入力画像

#include "opencv2/gapi.hpp"
#include "opencv2/gapi/core.hpp"
#include "opencv2/gapi/imgproc.hpp"
#include "opencv2/highgui.hpp"

int main(int argc, char **argv) {
    using namespace std;

    const string file_name = "lenna.jpg";
    const cv::Mat input_image = cv::imread(file_name, cv::IMREAD_COLOR);
    if(!input_image.empty()) {
        cv::Mat output_image = input_image.clone();
        // G-APIのグラフ定義では cv::Mat ではなく cv::GMat を利用する
        const cv::GMat in;
        const cv::GMat vga = cv::gapi::resize(in, cv::Size(), 0.5, 0.5);
        const cv::GMat gray = cv::gapi::BGR2Gray(vga);
        const cv::GMat blurred = cv::gapi::blur(gray, cv::Size(5,5));
        const cv::GMat edges = cv::gapi::Canny(blurred, 32, 128, 3);
        cv::GMat b,g,r;
        tie(b,g,r) = cv::gapi::split3(vga);
        const cv::GMat out = cv::gapi::merge3(b, g | edges, r);
        // 定義した計算グラフの適用
        cv::GComputation ac(in, out);
        ac.apply(input_image, output_image);
        cv::imwrite("gapi_output.jpg", output_image);
    } else {
        cout << "image can't read" << endl;
    }
    return 0;
}

* 出力画像

個々の画像処理を繋げてパイプラインを作っていることが読み取れます。このサンプルでは一連の処理をすぐに適用していますが、適用自体は結果が必要になる時まで遅らせることも出来ますし、実行時に途中の処理を他の処理に差し替えることもできます。途中の処理結果はconstにしていることからもわかるようにイミュータブルなオブジェクトとして扱うことができるのも嬉しいですね。適切な粒度でグラフを構築しておけば、データの不変性・局所性により、どこかのノードで計算処理に失敗した場合でも、処理が成功しているノードまで遡って続きから再実行することもできます。

定義した計算グラフを遅延評価したい場合は、cv::GComputation::applyの代わりにcv::GComputation::compileでグラフを事前構築してから、任意の場所で実行できます。

// 入力データのメタ情報(形式や解像度等)を cv::descr_of で取得
const auto meta = cv::descr_of(cv::gin(input_image));
// 計算グラフの構築 (構築時に入力データのメタ情報を渡す)
cv::GCompiled gc = ac.compile(cv::GMetaArgs(meta));
// cv::GCompiled オブジェクトはファンクタなので operator() で実行可能
gc(input_image, output_image);

また、2018/12現在、G-APIのPythonインタフェースは提供されていないようです。G-APIのC++インタフェースは、例えばApache SparkのRDD/DataFrame/Datasetを活用したコードと比較するとまだ洗練されていないように見えますし、そのままPython用にバインディングしてもスクリプト言語らしい手軽さで使えるようにするのは難しいかもしれません。

一応、グラフ定義部分はラムダ式を使うと以下のようにも書けます。一時オブジェクトのスコープが限定されるところは良いですけど、あんまり変わらない気も。

cv::GComputation ac([]() {
  cv::GMat in;
  auto vga = cv::gapi::resize(in, cv::Size(), 0.5, 0.5);
  auto gray = cv::gapi::BGR2Gray(vga);
  auto blurred = cv::gapi::blur(gray, cv::Size(5,5));
  auto edges = cv::gapi::Canny(blurred, 32, 128, 3);
  cv::GMat b,g,r;                                                                                                                                                                          
  tie(b,g,r) = cv::gapi::split3(vga);
  auto out = cv::gapi::merge3(b, g | edges, r);
  return cv::GComputation(in, out);
});
ac.apply(input_image, output_image);

また、G-APIの重要な機構のひとつに実行環境を透過的に選択できる点があります。2018/12現在、リファレンスを見る限りCPU/GPU(via OpenCL)/Fluidがサポート予定のようです。今回はCPU環境で試しましたが、次はGPU(CUDA)環境にインストールしていろいろ試す予定なので、G-APIの実行環境についてはまた別記事にしようと思います。

なんだかんだ書きましたけど、G-API、とても面白いです。OpenCV4.0時点ではまだ開発段階とのことなので今後の改善に期待したいです(まぁ僕自身がOpenCV開発に貢献できればもっと良いのですが)。

おわりに

OpenCV2.x系は仕事でもよく使ってたのですが、OpenCV3.x系は結局ほとんど使うことなく4.0を試すことになってしまいました。3.x系からの機能差分や旧バージョンとの非互換性等の整理をしておきたいところです。メジャーバージョンが上がっただけあって、多くの新機能追加、性能改善などが施されているようですので引き続きいろいろ試してみたいと思います。

今回作成したサンプルコードはGitHubにも上げてありますので興味があれば。

先月まで仕事が激務だったんですが、やっと余裕ができたのでブログ更新の頻度も上げられるといいなぁ。

あわせて読む:

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です