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 Manager → SDK Tools → NDK (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 algorithms が NO のままビルドすると、後述のサンプルはビルドもリンクも通るのに、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 に入れる .so は COMPONENTS に書いたものだけでは足りません。 できた libmylib.so の依存を NDK 付属の llvm-readelf -d で見ると、xfeatures2d が内部で使う ml / shape / calib3d / features2d / flann も NEEDED に並びます。同梱漏れは実機で起動したときに初めて分かるので、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)を開発しています。顔認証システムへの組み込み実績多数。

