2021年4月20日 星期二

在 Linux 系統上安裝 AndroidStudio 並加入 OpenCV 程式庫

先將 AndroidStudio 安裝完, 並下載 OpenCV 準備好:

1. 上 AndroidStudio 官網 https://developer.android.com/studio  下載壓縮包, 打開終端機, 進到 Downloads 目錄內, 解壓縮後進行安裝:

      cd Downloads

      tar  -xvf  android-studio-ide-201.7199119-linux.tar.gz

      cd  android-studio/bin &&  ./studio.sh

2. 執行 AndroidStudio (cd /home/mint/Downloads/android-studio/bin && ./studio.sh), 開啟新專案 -> 選擇 Native C++ -> 點選 Next -> Name 填入專案名稱  -> Finish, 當執行編譯時會要求安裝 NDK 編譯 c++  原始碼, 點選同意進行 NDK 的安裝

3. 再上 OpenCV 官網   https://opencv.org/releases/  點選 Android 下載 OpenCV SDK 壓縮包, 接著開啟終端機在 Downloads 目錄內解壓縮 :

     cd Downloads 

     unzip opencv-4.5.2-android-sdk.zip

執行 AndroidStudio, 打開上述 c++ 的專案, 透過 JNI 玩 OpenCV on android, 如果要使用 Java 版 openCV, 首先在Gradle Scripts 的頁籤 settings.gradle 增加以下內容:

     def opencvsdk='/home/mint/Downloads/OpenCV-android-sdk'    
     include ':opencv'
     project(':opencv').projectDir = new File(opencvsdk + '/sdk')

確認上述 opencvsdk 目錄正確, 改完後像這樣存檔 :

     def opencvsdk='/home/mint/Downloads/OpenCV-android-sdk'
     include ':app'
     include ':opencv'
     project(':opencv').projectDir = new File(opencvsdk + '/sdk')
     rootProject.name = "cvEx1"

點選 Sync-Now  同步更新 Gradle, 讓設定生效, 接著打開 Gradle Scripts 的頁籤 build.gradle(Module: ...) , 檔案內容拉到最後的地方, 加入內容像這樣:

     dependencies {

               implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
               implementation 'androidx.core:core-ktx:1.3.2'
               implementation 'androidx.appcompat:appcompat:1.2.0'
               implementation 'com.google.android.material:material:1.3.0'
               implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
               testImplementation 'junit:junit:4.+'
               androidTestImplementation 'androidx.test.ext:junit:1.1.2'
               androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
               implementation project(":opencv")

    }

同步後就會自動下載程式庫, 再將主程式 MainActivity.kt 稍作修改, 載入函式庫之後, 就能呼叫 OpenCV 類型的函數:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
import org.opencv.android.OpenCVLoader
import org.opencv.core.Mat
import org.opencv.core.CvType.CV_32F

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val txt = findViewById<TextView>(R.id.sample_text)
        if(!OpenCVLoader.initDebug()) return
        val jMat = Mat(5, 5, CV_32F)
       
txt.setText("${stringFromJNI()}, jMat: ${jMat.rows()} * ${jMat.cols()} sucessful")
    }
    external fun stringFromJNI(): String
    companion object {
        init {
            System.loadLibrary("native-lib")
        }
    }
}

重新編譯上述專案, 並上傳到手機, 測試看是否可以成功加載 openCV 的 Java 程式庫.

上述是在 kotlin(Java)語言中呼叫 Java 版 OpenCV 函數, 下一步要在原生 c++ (native-lib)程式碼中直接呼叫 OpenCV 函數, 必須修改 app/cpp/CMakeLists.txt (實際的檔案位置是 app/src/main/cpp/CMakeLists.txt),引入標頭檔及程式庫,將以下紅色內容插入 CMakeLists.txt, 像這樣:

    cmake_minimum_required(VERSION 3.10.2)
    project("cvex1")
    set(OpenCV_DIR  /home/mint/Downloads/OpenCV-android-sdk/sdk/native/jni)
    find_package(OpenCV  REQUIRED)

    include_directories(${OpenCV_INCLUDE_DIRS})

    add_library(native-lib SHARED native-lib.cpp)
    find_library(log-lib log)
    target_link_libraries(native-lib   -ljnigraphics   ${OpenCV_LIBS}  ${log-lib})

接著修改 app/cpp/native-lib.cpp (實際的檔案位置是 app/src/main/cpp/native-lib.cpp),內容像這樣:

#include <jni.h>
#include <string>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_cvex1_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    Mat cMat(200, 320, CV_8UC3);
    char buffer[256];
    sprintf(buffer, "Hello C++ cMat:%d * %d", cMat.rows, cMat.cols);
    return env->NewStringUTF(buffer);

}

重新編譯上述專案, 並上傳到手機, 測試看是否可以成功連結 C++ 的程式庫. 到此已萬事俱備.

後記: 若只想在 native-lib 內使用 openCV 函式庫處理影像甚至在作畫, 只要修改 CMakeLists.txt 及 native-lib.cpp 就夠了, 前面所說的設定 settings.gradle 就不用了, 在 build.gradle 內添加的 opencv 程式庫: dependencies { ...  implementation project(":opencv" } 也要跟著移除, 要是將兩個程式庫(C++版 及 JAVA 版)都收編進 Android app程式內, 似乎也可以, 但會有警告的訊息. 也不知道會有何副作用? 底下是一個直接在 bitmap (ARGB_8888 格式)作畫的程式碼 (native-lib.cpp):

#include <jni.h>
#include <string>
#include "opencv2/objdetect.hpp"
#include <opencv2/imgproc.hpp>
#include <opencv2/core/types_c.h>
#include <opencv2/highgui/highgui.hpp>
#include <chrono>
#include <android/bitmap.h>
using namespace cv;
using namespace std;
using namespace std::chrono;
int fps( ) {
    static auto lastTime = system_clock::now( ); // .time_since_epoch();
    static long nSxcount = 0;
    static int frames = 0;
    static int _fps = 0;
    auto currentTime = system_clock::now( );
    nSxcount += nanoseconds(system_clock::now( ) - lastTime).count( );// 累積時間差nS
    if (++ frames == 10) {// 累積10張, 算一次 fps, 並且避免數字跳動
        _fps  = 1e10/(nSxcount + 5);// 4捨5入, 並且避免除0錯誤
        frames = 0;
        nSxcount = 0;
    }
    lastTime = currentTime;// to be used next time
    return _fps;
}
extern "C" JNIEXPORT jstring JNICALL Java_com_example_cvex1_MainActivity_fpsdraw(
        JNIEnv *env,
        jobject _this,
        jstring appPath,
        jobject bitmap = NULL
    ) {
    static int id = 0;
    char buffer[1024];
    buffer[0] = 0;
    if (bitmap != NULL) {
        AndroidBitmapInfo info;
        AndroidBitmap_getInfo(env, bitmap, &info);
        if (info.format == ANDROID_BITMAP_FORMAT_RGBA_8888) {
            void *array;
            AndroidBitmap_lockPixels(env, bitmap, &array);
            Mat mat(info.height, info.width, CV_8UC4, array);
            sprintf(buffer, "fps=%3d
run@%02d", fps( ), (id ++)%100);
            putText(mat, String(buffer), Point(0, 100), 1, 4.0, Scalar(0, 255, 0));// 4x,Green color

            AndroidBitmap_unlockPixels(env, bitmap);
        }
    }
    return env->NewStringUTF(buffer);
}

主程式 MainActivity.kt:

package com.example.cvex1
import android.graphics.Bitmap
import android.graphics.Color
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.widget.ImageView
import android.widget.TextView

class MainActivity : AppCompatActivity( ) {
    private external fun fpsdraw(pathDirectory: String, bitmap: Bitmap?): String
    private lateinit var textView: TextView
    private lateinit var imageView: ImageView
    private lateinit var bitmap:Bitmap
   
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        if(savedInstanceState == null) {
            imageView = findViewById<ImageView>(R.id.imageView)
            textView = findViewById<TextView>(R.id.sample_text) .apply {
                setTextColor(Color.RED)
                textSize = 20f
            }
            bitmap = Bitmap.createBitmap(720, 720, Bitmap.Config.ARGB_8888)// w720xh720
        }
    }   
    private fun setBitmap( ) {
        bitmap.apply {// this is bitmap
            eraseColor(0) // fill bitmap with black color
            textView.text = fpsdraw(filesDir.path , this) // textView follows bitmap update
            imageView.setImageBitmap(this) // imageView update
            Handler(mainLooper).post { setBitmap( ) }// loop this function again
        }
    }
    override fun onResume( ) {
        super.onResume( )
        setBitmap( )
    }   
    companion object {
        init { System.loadLibrary("native-lib") }
    }
}

主視窗外觀檔 activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app  ="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width     ="match_parent"
    android:layout_height    ="match_parent"
    android:background       ="@color/black"
    tools:context=".MainActivity">
    <ImageView
        android:id ="@+id/imageView"
        android:layout_width ="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf  ="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf ="parent"/>
    <TextView
        android:id ="@+id/sample_text"
        android:layout_width ="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf  ="parent"
        app:layout_constraintTop_toBottomOf ="@+id/imageView"/>
</androidx.constraintlayout.widget.ConstraintLayout>

編譯完上傳到 Android 手機, 每秒更新速度可以達到將近 60 張(fps)

後記: 一吐不快超級無敵慢的 Android Studio

1. 安裝 Android Studio 要用 SSD (固態硬碟), 否則啟動時將會慢到無法忍受, 改用 SSD 就能感受到它所帶來的快感, 估計可能是 gradle 運行時, 硬碟中太多檔案需要搜索,導致花費太多時間.

2. 隨機記憶體(DRAM)最好加裝到 8G 以上, 16G 則綽綽有餘, 4G 勉強可以運行.

3. 若只有 4G 的記憶體, 可以考慮不執行 Android Studio 來節省記憶體之消耗, 直接在命令列用 gradlew 來編譯就可. 若非得用 Android Studio, 那就不能再開啟其它視窗執行程式. 在 linux 系統上可以啟用 swap partition (記憶體交換分區)來增加執行空間, 但相對的執行速度就會慢很多.

4. 我用 Android Studio 唯一的時機是用它校正語法輸入錯誤, 他的編輯方式對習慣用 vscode 的我來說, 最大敗筆是動不動跳出一些莫名其妙的提示, 就像正在開車時,旁邊副駕駛在旁邊嘮叨,讓你無奈到快要抓狂.

沒有留言:

張貼留言

使用 pcie 轉接器連接 nvme SSD

之前 AM4 主機板使用 pcie ssd, 但主機板故障了沒辦法上網, 只好翻出以前買的 FM2 舊主機板, 想辦法讓老主機復活, 但舊主機板沒有 nvme 的界面, 因此上網買了 pcie 轉接器用來連接 nvme ssd, 遺憾的是 grub2 bootloader 無法識...