OpenCV + opencv_contrib を Android 向けにビルドする完全ガイド【C++/NDK】

OpenCV for C++
📌 準備: OpenCV の環境構築がまだの方はこちら → C++ 環境構築ガイドC#(OpenCvSharp)セットアップ

OpenCV + opencv_contrib を Android 向けにビルドする

この記事では、OpenCV 本体と opencv_contrib を Android NDK でクロスコンパイルし、NDK のプロジェクトから使える .so を作るまでの手順を説明します。

OpenCV 公式の Android 向け配布物(prebuilt SDK)には contrib モジュールが含まれていません。SURF(xfeatures2d)・ximgproc・文字認識(text)など contrib にしか無い機能を C++ から使うには、自分でビルドする必要があります。

この記事の手順は、macOS 上で実際に CMake 構成 → ビルド → インストール → アプリ側からのリンクまで通したものです。


前提環境

項目 内容
ホスト macOS(Apple M2・8 コア)
CMake 4.4.3(Homebrew)
Android NDK r26d(26.3.11579264)
OpenCV / opencv_contrib 4.10.0(タグを一致させる)
ターゲット ABI arm64-v8a

当社の SDK 配布用ビルドも NDK r26d を使っています。より新しい NDK を使う場合は、CMake 構成の出力にある Android NDK: の行で、実際に使われた版を確認してください。

Linux ホストでも同じ CMake オプションで構成できる作りですが、本記事では Linux ホストでのビルドは実行していません。


手順

1. ツールの準備

brew install cmake

git は Xcode Command Line Tools に含まれています。

Android 向けビルドはクロスコンパイルなので、ホスト側の GUI ライブラリや FFmpeg 開発パッケージは不要です。ホスト用の OpenCV を入れる手順を流用して、使わないパッケージまで入れているケースをよく見かけます。

2. ソースコードの取得

本体と contrib は必ず同じタグにします。

git clone https://github.com/opencv/opencv.git
git clone https://github.com/opencv/opencv_contrib.git

cd opencv         && git checkout 4.10.0 && cd ..
cd opencv_contrib && git checkout 4.10.0 && cd ..

3. NDK の配置確認

Android Studio の SDK ManagerSDK ToolsNDK (Side by side) から入れるのが確実です。インストール先は ~/Library/Android/sdk/ndk/<バージョン>/ になります。コマンドラインなら次のとおりです。

sdkmanager "ndk;26.3.11579264"

環境変数を通します(~/.zshrc に追記)。

export ANDROID_NDK_ROOT=~/Library/Android/sdk/ndk/26.3.11579264

toolchain ファイルがあることを確認しておきます。

ls $ANDROID_NDK_ROOT/build/cmake/android.toolchain.cmake

パスを間違えたまま次の cmake を実行すると、Could not find toolchain file: ですぐに止まります。環境変数を設定し忘れた場合も、パスが /build/cmake/android.toolchain.cmake になって同じエラーです。危ないのは -DCMAKE_TOOLCHAIN_FILE 自体を書き忘れたときで、この場合はエラーにならず、ホスト(macOS)向けのビルドがそのまま始まります。

4. CMake 構成

mkdir -p opencv/build_android && cd opencv/build_android
cmake \
  -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_ROOT/build/cmake/android.toolchain.cmake \
  -DANDROID_ABI=arm64-v8a \
  -DANDROID_PLATFORM=android-28 \
  -DANDROID_STL=c++_shared \
  -DOPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules \
  -DOPENCV_ENABLE_NONFREE=ON \
  -DBUILD_SHARED_LIBS=ON \
  -DBUILD_JAVA=OFF \
  -DBUILD_ANDROID_PROJECTS=OFF \
  -DBUILD_ANDROID_EXAMPLES=OFF \
  -DBUILD_TESTS=OFF \
  -DBUILD_PERF_TESTS=OFF \
  -DBUILD_opencv_apps=OFF \
  ..

主要オプションの意味

オプション 説明
ANDROID_ABI ターゲット ABI。armeabi-v7a / x86_64 / x86 も指定できる
ANDROID_PLATFORM 最低 API レベル。当社は 28(Android 9.0)を下限にしている
ANDROID_STL c++_shared / c++_staticどちらが正しいかは .so の数で決まる(後述)
OPENCV_EXTRA_MODULES_PATH contrib モジュールのパス
OPENCV_ENABLE_NONFREE=ON SURF など特許関連アルゴリズムを有効化。付けないとビルドは通るが、SURF::create() が実行時に例外を投げる
BUILD_SHARED_LIBS=ON Android では既定が OFF(静的ライブラリ)なので明示する
BUILD_JAVA=OFF / BUILD_ANDROID_PROJECTS=OFF C++ から使うだけなら必須(次項)

BUILD_JAVA=OFF を付けないと Android SDK を要求される

Android 向けでは Java ラッパーのビルドが既定で有効です。有効なままだと CMake が Android SDK を探しに行き、見つからなければ構成の段階で止まりますAndroid SDK: specify path to Android SDK via ANDROID_SDK_ROOT / ANDROID_HOME / ANDROID_SDK variables)。NDK だけを入れた環境で起きやすい失敗です。

C++(NDK)から使うだけなら、Java ラッパーも Gradle のサンプルプロジェクトも要りません。上の2つを OFF にすると、構成の出力は次のようになり、SDK を参照しなくなります。

--   Android SDK:                   not used, projects are not built

Kotlin / Java から OpenCV を呼びたい場合は、この2つを ON のままにして Android SDK を用意してください。

構成の出力では、ほかに次の3点を確認しておきます。

--     STL type:                    c++_shared
--     Non-free algorithms:         YES
--   Install to:                    .../opencv/build_android/install

Non-free algorithmsNO のままビルドすると、後述のサンプルはビルドもリンクも通るのに、SURF を呼んだ瞬間に例外でアプリごと落ちます。エミュレータで試すと、logcat に次のように出て強制終了しました。ビルド前に確認してください。

libc++abi: terminating due to uncaught exception of type cv::Exception: OpenCV(4.10.0) ...
... This algorithm is patented and is excluded in this configuration; Set OPENCV_ENABLE_NONFREE CMake option and rebuild the library ...

opencv_sfm は依存ライブラリ(Eigen / Glog / Gflags)が揃わないため、Android では 自動的に対象から外れますModule opencv_sfm disabled because the following dependencies are not found)。何も指定しなくて構いません。

5. ビルド

cmake --build . --parallel $(sysctl -n hw.logicalcpu)

make -j$(nproc) と書く手順もよく見かけますが、nproc は macOS にはありません。Linux ホストなら $(nproc) で構いません。

⚠️ --parallel の後ろの数値は省略しないでください。 CMake の既定(Makefile ジェネレータ)では、数値を省くと並列数の上限が無くなります。OpenCV + contrib の規模だとコンパイラが数十本同時に起動し、当社の検証でも Mac(M2・メモリ 16GB)がスワップを使い切って操作できなくなりました。

contrib を全部含めた構成で、Apple M2(8 コア)で CMake 構成 24 秒・ビルド 4分06秒でした(並列数 8)。ほぼ同じ構成を並列数の指定なしで実行したときは、スワップの影響で 27 分以上かかっています。

6. インストール

cmake --install . --strip

cmake --install は CMake 3.15 以降の機能です。)

クロスコンパイルでは、インストール先の既定が ビルドディレクトリ直下の install/ になります。make install DESTDIR=... のように別の場所を重ねると、install/<絶対パス>/.../install/ と二重の階層になるので付けません。

できあがる構成は次のとおりです。

build_android/install/
└── sdk/
    ├── etc/                       ← カスケード分類器・ライセンス
    └── native/
        ├── jni/
        │   ├── OpenCVConfig.cmake        ← CMake の find_package 用
        │   ├── OpenCVConfig-version.cmake
        │   ├── OpenCV.mk                 ← ndk-build 用
        │   ├── OpenCV-arm64-v8a.mk
        │   ├── abi-arm64-v8a/
        │   └── include/opencv2/          ← ヘッダ
        └── libs/
            └── arm64-v8a/
                ├── libopencv_core.so
                ├── libopencv_xfeatures2d.so   ← contrib
                └── ...(計 52 個)

--strip を付けないと約 400MB になる

モジュールごとの .so が 52 個できますが、--strip を付けないと arm64-v8a だけで約 400MB ありました。デバッグ情報が残っているためで(file コマンドで with debug_info, not stripped)、--strip を付けると 37MB になります。APK の容量に直結するので、配布用には必ず strip します。

なお、ANDROID_STL=c++_shared でビルドしても、libc++_shared.so はこのインストール先には入りません。アプリ側に同梱されていることを確認してください。


アプリ側から使う

Android アプリの CMake(externalNativeBuild)からは、OpenCVConfig.cmake のあるディレクトリを指定して find_package で読み込みます。

set(OpenCV_DIR "${CMAKE_SOURCE_DIR}/../opencv/build_android/install/sdk/native/jni")
find_package(OpenCV REQUIRED COMPONENTS core imgproc xfeatures2d)

add_library(mylib SHARED native-lib.cpp)
target_link_libraries(mylib PRIVATE ${OpenCV_LIBS} android log)

COMPONENTS を付けないと ${OpenCV_LIBS} に全モジュールが入ります(詳しくは CMake で OpenCV をリンクする方法【find_package 完全ガイド・C++】)。

JNI 側のコードはたとえば次のようになります。

#include <jni.h>
#include <android/log.h>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/xfeatures2d.hpp>

extern "C" JNIEXPORT jint JNICALL Java_com_example_app_MainActivity_countEdges(JNIEnv*, jobject)
{
    cv::Mat img(240, 320, CV_8UC1, cv::Scalar(0));
    cv::rectangle(img, cv::Rect(80, 60, 160, 120), cv::Scalar(255), cv::FILLED);

    cv::Mat edges;
    cv::Canny(img, edges, 50.0, 150.0);

    auto surf = cv::xfeatures2d::SURF::create();  // contrib が使えることの確認
    __android_log_print(ANDROID_LOG_INFO, "OpenCV", "OpenCV %s", CV_VERSION);
    return cv::countNonZero(edges);
}

NDK r26d / arm64-v8a で上の CMake からビルドし、Android 13(arm64-v8a)のエミュレータ上で Java から呼び出して、countEdges() = 556 が返ることを確認しています(logcat には OpenCV 4.10.0 が出力されます)。

⚠️ JNI 関数には extern "C" を付けてください。 .cpp ファイルに書くと C++ の名前修飾が付き、付けないままビルドするとエクスポート名が _Z44Java_com_example_app_MainActivity_countEdgesP7_JNIEnvP8_jobject のようになります。ビルドもリンクも通りますが、Java から呼んだ時点で次のエラーになります(エミュレータで確認)。

java.lang.UnsatisfiedLinkError: No implementation found for int com.example.app.MainActivity.countEdges() (tried Java_com_example_app_MainActivity_countEdges and ...)

⚠️ APK に入れる .soCOMPONENTS に書いたものだけでは足りません。 できた libmylib.so の依存を NDK 付属の llvm-readelf -d で見ると、xfeatures2d が内部で使う ml / shape / calib3d / features2d / flannNEEDED に並びます。同梱漏れは実機で起動したときに初めて分かるので、NEEDED を見て洗い出すのが確実です。

ホスト側で画像処理ロジックを先に確かめる

JNI や NDK 専用ヘッダ(<jni.h> / <android/log.h>)を混ぜると、ホストではコンパイルできません。画像処理の部分だけを先にホストで完成させてから JNI に載せると、「処理の誤りなのか、クロスコンパイル環境の問題なのか」を切り分けやすくなります。

#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/imgcodecs.hpp>

// JNI から呼ぶ予定の処理(ホストでも単体で確認できる)
void processFrame(const cv::Mat& src, cv::Mat& dst)
{
    if (src.empty()) {
        return;
    }

    cv::Mat gray;
    cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);          // グレースケール変換

    cv::Mat blurred;
    cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 1.5); // ノイズ除去

    cv::Canny(blurred, dst, 50.0, 150.0);                 // エッジ検出
}

int main()
{
    cv::Mat src = cv::imread("test.png");
    if (src.empty()) {
        return 1;
    }

    cv::Mat result;
    processFrame(src, result);
    cv::imwrite("result.png", result);
    return 0;
}
g++ -std=c++17 main.cpp $(pkg-config --cflags --libs opencv4) -o main && ./main

result.png にエッジ画像が出れば、処理部分はできています。macOS の Homebrew で OpenCV 4 系の pkg-config を使う場合の注意は OpenCV + opencv_contrib を macOS でビルドする完全ガイド【C++/CMake】 にまとめています。


つまずきポイント

⚠️ ANDROID_STL は「.so が1つか複数か」で決まる

c++_shared を無条件に勧める記事が多いのですが、正しい選択は構成で変わります。

構成 指定 理由
モジュールごとに .so が複数(本記事の構成) c++_shared STL を静的に持つ .so が複数あると、例外や RTTI が .so をまたいだときに正しく動かない
.so を1つにまとめる(BUILD_opencv_world=ON c++_static も選べる STL をその1つに埋め込めるので、libc++_shared.so を別に同梱しなくて済む

c++_static を選ぶ場合は、アプリの他の .so も含めて、STL を静的に持つ .so が1つだけという条件を守る必要があります。

⚠️ .so を1つにまとめたい場合(当社の配布用ビルド)

当社の SDK 配布用ビルドは、BUILD_opencv_world=ON で1ファイルにまとめ、BUILD_LIST でモジュールを絞り、ANDROID_STL=c++_static にしています。

  -DBUILD_opencv_world=ON \
  -DBUILD_LIST=core,imgproc,dnn,objdetect,imgcodecs \
  -DANDROID_STL=c++_static \

参考までに、この構成(contrib なし・5 モジュール・strip 済み)での実サイズです。本記事の構成(contrib 全部・モジュール別)とは別物なので、規模感として見てください。

ABI world ライブラリ(参考値)
arm64-v8a 18 MB
armeabi-v7a 13 MB
x86_64 50 MB

⚠️ world ビルドの出力名は既定で libopencv_world.so です。当社はリンク時に soname を指定して libopencv_world4100.so という名前にして配布しています(既定ではこの名前になりません)。


まとめ

  • -DCMAKE_TOOLCHAIN_FILE に NDK の android.toolchain.cmake を渡してクロスコンパイルする。書き忘れるとエラーにならずホスト向けビルドになる
  • C++ から使うだけなら -DBUILD_JAVA=OFF -DBUILD_ANDROID_PROJECTS=OFF。付けないと Android SDK を要求される
  • SURF などを使うなら -DOPENCV_ENABLE_NONFREE=ON。付けないとビルドは通るのに実行時に例外になる
  • JNI 関数には extern "C"。付けないと Java から呼べない(UnsatisfiedLinkError
  • ビルドは cmake --build . --parallel <並列数>数値を省くと並列数が無制限になる。macOS に nproc は無い
  • インストールは cmake --install . --strip。strip しないと arm64-v8a だけで約 400MB、strip すると 37MB
  • アプリには COMPONENTS に書いたモジュールだけでなく、その依存先の .so も同梱する(llvm-readelf -d で確認)
  • ANDROID_STL.so が1つか複数かで決める

ホスト用(Linux)のビルド手順は OpenCV + opencv_contrib を Ubuntu でビルドする完全ガイド【C++】 を参照してください。

🛠 画像処理のプロが開発するSDK/API

本ブログを運営するスワローインキュベートは、OpenCV ベースのなりすまし判定SDK/API(C++製・OpenCV 4.10)を開発しています。顔認証システムへの組み込み実績多数。

→ なりすまし判定SDK/APIの詳細を見る
→ API仕様書・サンプルコード

タイトルとURLをコピーしました