【手把手AI项目】八、MobileNetSSD通过Ncnn前向推理框架在Android端的使用--Cmake编译(目标检测)下

android studio 相关说明 | 2019-03-23 19:06

上一篇:

请看完上一篇再来接着看这一篇

下面代码自己仿照ncnn的examples里的squeezencnn的安卓项目工程里面的代码所修改,这个项目工程是利用NDK-build不是我所讲的Cmake方式

具体代码如下(有详细注释)

1#include <android/bitmap.h> 2#include <android/log.h> 3#include <jni.h> 4#include <string> 5#include <vector> 6 7// ncnn 8#include "include/opencv.h" 9#include "MobileNetSSD_deploy.id.h"   //这里看成自己的id.h 10#include <sys/time.h> 11#include <unistd.h> 12#include "include/net.h" 13 14static ncnn::UnlockedPoolAllocator g_blob_pool_allocator; 15static ncnn::PoolAllocator g_workspace_pool_allocator; 16 17static ncnn::Mat ncnn_param; 18static ncnn::Mat ncnn_bin; 19static ncnn::Net ncnn_net; 20 21extern "C" { 22 23// public native boolean Init(byte[] words,byte[] param, byte[] bin);  原函数形式(c++) 以下形式为ndk的c++形式 24JNIEXPORT jboolean JNICALL 25Java_com_example_che_mobilenetssd_demo_MobileNetssd_Init(JNIEnv *env, jobject obj, jbyteArray param, jbyteArray bin){ 26    __android_log_print(ANDROID_LOG_DEBUG, "MobileNetssd", "enter the jni func"); 27    // init param 28    { 29        int len = env->GetArrayLength(param); 30        ncnn_param.create(len, (size_t) 1u); 31        env->GetByteArrayRegion(param, 0, len, (jbyte *) ncnn_param); 32        int ret = ncnn_net.load_param((const unsigned char *) ncnn_param); 33        __android_log_print(ANDROID_LOG_DEBUG, "MobileNetssd", "load_param %d %d", ret, len); 34    } 35 36    // init bin 37    { 38        int len = env->GetArrayLength(bin); 39        ncnn_bin.create(len, (size_t) 1u); 40        env->GetByteArrayRegion(bin, 0, len, (jbyte *) ncnn_bin); 41        int ret = ncnn_net.load_model((const unsigned char *) ncnn_bin); 42        __android_log_print(ANDROID_LOG_DEBUG, "MobileNetssd", "load_model %d %d", ret, len); 43    } 44 45    ncnn::Option opt; 46    opt.lightmode = true; 47    opt.num_threads = 4;   //线程 这里可以修改 48    opt.blob_allocator = &g_blob_pool_allocator; 49    opt.workspace_allocator = &g_workspace_pool_allocator; 50 51    ncnn::set_default_option(opt); 52 53    return JNI_TRUE; 54} 55 56// public native String Detect(Bitmap bitmap); 57JNIEXPORT jfloatArray JNICALL Java_com_example_che_mobilenetssd_demo_MobileNetssd_Detect(JNIEnv* env, jobject thiz, jobject bitmap) 58{ 59    // ncnn from bitmap 60    ncnn::Mat in; 61    { 62        AndroidBitmapInfo info; 63        AndroidBitmap_getInfo(env, bitmap, &info); 64//        int origin_w = info.width; 65//        int origin_h = info.height; 66//        int width = 300; 67//        int height = 300; 68        int width = info.width; 69        int height = info.height; 70        if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) 71            return NULL; 72 73        void* indata; 74        AndroidBitmap_lockPixels(env, bitmap, &indata); 75        // 把像素转换成data,并指定通道顺序 76        // 因为图像预处理每个网络层输入的数据格式不一样一般为300*300 128*128等等所以这类需要一个resize的操作可以在cpp中写,也可以是java读入图片时有个resize操作 77//      in = ncnn::Mat::from_pixels_resize((const unsigned char*)indata, ncnn::Mat::PIXEL_RGBA2RGB, origin_w, origin_h, width, height); 78 79        in = ncnn::Mat::from_pixels((const unsigned char*)indata, ncnn::Mat::PIXEL_RGBA2RGB, width, height); 80 81        // 下面一行为debug代码 82        //__android_log_print(ANDROID_LOG_DEBUG, "MobilenetssdJniIn", "Mobilenetssd_predict_has_input1, in.w: %d; in.h: %d", in.w, in.h); 83        AndroidBitmap_unlockPixels(env, bitmap); 84    } 85 86    // ncnn_net 87    std::vector<float> cls_scores; 88    { 89        // 减去均值和乘上比例(这个数据和前面的归一化图片预处理形式一一对应) 90        const float mean_vals[3] = {127.5f, 127.5f, 127.5f}; 91        const float scale[3] = {0.f, 0.f, 0.f}; 92 93        in.substract_mean_normalize(mean_vals, scale);// 归一化 94 95        ncnn::Extractor ex = ncnn_net.create_extractor();//前向传播 96 97        // 如果不加密是使用ex.input("data", in); 98        // BLOB_data在id.h文件中可见,相当于datainput网络层的id 99        ex.input(MobileNetSSD_deploy_param_id::BLOB_data, in);100        //ex.set_num_threads(4); 和上面一样一个对象101102        ncnn::Mat out;103        // 如果时不加密是使用ex.extract("prob", out);104        //BLOB_detection_out.h文件中可见,相当于dataout网络层的id,输出检测的结果数据105        ex.extract(MobileNetSSD_deploy_param_id::BLOB_detection_out, out);106107        int output_wsize = out.w;108        int output_hsize = out.h;109110        //输出整理111        jfloat *output[output_wsize * output_hsize];112        for(int i = 0; i< out.h; i++) {113            for (int j = 0; j < out.w; j++) {114                output[i*output_wsize + j] = &out.row(i)[j];115            }116        }117        jfloatArray jOutputData = env->NewFloatArray(output_wsize);118        if (jOutputData == nullptr) return nullptr;119        env->SetFloatArrayRegion(jOutputData, 0,  output_wsize * output_hsize,120                                 reinterpret_cast<const jfloat *>(*output));121122        return jOutputData;123    }124}125}将.cpp文件放到cpp文件夹下

1    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>2    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>3    <uses-permission android:name="android.permission.CAMERA"/>3.CMakeLists.txt修改 1# For more information about using CMake with Android Studio, read the 2# documentation: -native-code.html 3 4# Sets the minimum version of CMake required to build the native library. 5 6cmake_minimum_required(VERSION 3.4.1) 7 8# Creates and names a library, sets it as either STATIC 9# or SHARED, and provides the relative paths to its source code.10# You can define multiple libraries, and CMake builds them for you.11# Gradle automatically packages shared libraries with your APK.1213##需要添加 相当于添加ncnn for android 的包 need to add14set(ncnn_lib ${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi-v7a/libncnn.a)15add_library (ncnn_lib STATIC IMPORTED)16set_target_properties(ncnn_lib PROPERTIES IMPORTED_LOCATION ${ncnn_lib})1718add_library( # Sets the name of the library.19        MobileNetssd ## 为生成.so的文字最好直接和.cpp名字一样,需要更改 need to add2021        # Sets the library as a shared library.22        SHARED2324        # Provides a relative path to your source file(s).25        src/main/cpp/MobileNetssd.cpp )##cpp文件的name2627# Searches for a specified prebuilt library and stores the path as a28# variable. Because CMake includes system libraries in the search path by29# default, you only need to specify the name of the public NDK library30# you want to add. CMake verifies that the library exists before31# completing its build.3233find_library( # Sets the name of the path variable.34        log-lib3536        # Specifies the name of the NDK library that37        # you want CMake to locate.38        log)3940# Specifies libraries CMake should link to your target library. You41# can link multiple libraries, such as libraries you define in this42# build script, prebuilt third-party libraries, or system libraries.4344target_link_libraries( # Specifies the target library.45        ##以下三个都要添加 need to add46        MobileNetssd   #和上面一样47        ncnn_lib       #这个ncnn的lib的add48        jnigraphics    #这个jni也需要add4950        # Links the target library to the log library51        # included in the NDK.52        ${log-lib})534.build.gradle修改 1apply plugin: 'com.android.application' 2 3android { 4    compileSdkVersion 28 5    defaultConfig { 6        applicationId "com.example.che.mobilenetssd_demo" 7        minSdkVersion 15 8        targetSdkVersion 28 9        versionCode 110        versionName "1.0"11        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"12        externalNativeBuild {13            cmake {14                cppFlags "-std=c++11 -fopenmp"//c++,多线程 需要添加 need to add15                abiFilters "armeabi-v7a" // 手机的硬件架构,基本所有的硬件都适配 need to add16            }17        }18    }19    buildTypes {20        release {21            minifyEnabled false22            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'23        }24    }25    externalNativeBuild {26        cmake {27            path "CMakeLists.txt"28        }29    }3031    // 需要添加 把 .a文件导入, .a为 ncnn make intall生成的里面的.a文件 need to add32    sourceSets {33        main {34            jniLibs.srcDirs = ["src/main/jniLibs"]35            jni.srcDirs = ['src/cpp']36        }37    }38}3940dependencies {41    implementation fileTree(dir: 'libs', include: ['*.jar'])42    implementation 'com.android.support:appcompat-v7:28.0.0'43    implementation 'com.android.support.constraint:constraint-layout:1.1.3'44    testImplementation 'junit:junit:4.12'45    implementation 'com.github.bumptech.glide:glide:4.3.1'   // 需要添加,增加图片类 bumptech,build自动红线消失 need to add46    androidTestImplementation 'com.android.support.test:runner:1.0.2'47    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'48}495.自己编写java接口命名为和.cpp文件一样的名称这里就是MobileNetssd.java

1package com.example.che.mobilenetssd_demo; 2 3import android.graphics.Bitmap; 4 5/** 6 *  MobileNetssd的java接口,与本地c++代码相呼应 native为本地 此文件与 MobileNetssd.cpp相呼应 7 */ 8public class MobileNetssd { 910    public native boolean Init(byte[] param, byte[] bin); // 初始化函数 11    public native float[] Detect(Bitmap bitmap); // 检测函数12    // Used to load the 'native-lib' library on application startup.13    static {14        System.loadLibrary("MobileNetssd");//最后15    }16}public native boolean Init(byte[] param, byte[] bin); // 初始化函数 对应于NDK编写的.cpp文件中的JNIEXPORT jboolean JNICALLJava_com_example_che_mobilenetssd_demo_MobileNetssd_Init(JNIEnv *env, jobject obj, jbyteArray param, jbyteArray bin)

public native float[] Detect(Bitmap bitmap); // 检测函数 对应于NDK编写的.cpp文件中的JNIEXPORT jfloatArray JNICALL Java_com_example_che_mobilenetssd_demo_MobileNetssd_Detect(JNIEnv* env, jobject thiz, jobject bitmap)其实有规律可寻以第二个为例子函数前三个都是JNIEXPORT +函数返回类型(NDK形式)+JNICALLJava_com_example_che_mobilenetssd_demo_MobileNetssd_Detect(JNIEnv* env, jobject thiz, jobject bitmap)对于这个命名先看下面这个图你可能就懂了

相当于绝对路径那种感觉了显示java文件夹,再是com.example.che.mobilenetssd_demo这个包再是MobileNetssd.java文件最后是 java接口文件中的public native float[] Detect(Bitmap bitmap); 函数的文件名,即Java_com_example_che_mobilenetssd_demo_MobileNetssd_Detect,后面参数则为(JNIEnv* env, jobject thiz,+原函数的函数参数(NDK类型格式))其实正常来讲NDK这种不应该是先写cpp再写java接口而是应该先写java接口再利用IDE本身带有的NDK的开发环境可以直接双点写的java接口的函数(函数格式如上需要加一个native)之后快捷键按ALT+ENTER即可直接在cpp文件中添加成功,具体请看NDK技术即可,这里不赘余了。接下来就是我的其他.java文件以及UI的XML文件了

点击Build,之后点击Make Project,等一会,不出意外应该是成功了

查看自己的这个文件夹的对应的编译硬件架构的文件夹下成功生成.so文件,名字为libMobileNetssd.so

我喜欢用真机测试,插上真机打开开发者模式等等,可百度一下,很多,不赘述了先导入一些测试图片,之后run 就好了,这个地方太基本不想多说了,没说过android的朋友稍微探索一下怎么成功导入app即可,这过程中可能因为手机版本等更换SDK版本等等问题,常见的就是修改sdk版本即可,在导入的过程中需要一些手机真机的钥匙等确认。安装完毕打开app有几个权限需要手机确认以下即可,真机照片(这里会出现一个无法编译成功的错误,看总结里写到解决方式,主要和NDK命名规则有关)

前面提到一个 线程的问题,这里可以自行更改到速度最快即可,一般为4最快,这个也和你输入的图片

这边的代码目前只支持单目标检测,现在一张图片中有多个目标则无法成功检测出最后的框图版本,下一篇博客把这个实现写上准备,这里还有一点就是.cpp文件中的NDK函数的名称需要修改才能最后app装在手机上成功,我项目的android名称为一连串的,而这回起名为MobileNetSSD_demo形式(感觉自己也是没事找事= =),中多个下划线,这里直接关系到NDK的函数命名格式

1Java_com_example_che_mobilenetssd_demo_MobileNetssd_Init(原来的)2Java_com_example_che_mobilenetssd_1demo_MobileNetssd_Init(更改的)在mobilenetssd_后多个1再加上demo才能关联成功,java接口的这个函数直接go to Declaration就可以直接关联到.cpp中这个函数了,Detect函数以此列推,如果你自己再走一遍流程起名是一连串的(比如MobileNetssddemo)就不会出现这样的问题了,但是这个问题不会影响之前的所有讲解,没有问题,改完之后需要clean project 之后重新make project 即可成功 成功结果如上图所示

已在我的github上,希望大家给个star follow.因为准备和下一篇博文多检测的分开重命名为single

PS: 如果觉得本篇本章对您有所帮助,欢迎关注、评论、点赞!Github给个Star就更完美了_!

如果想打包成android APK请参照我另一篇博文

保持谦逊,保持自律,保持进步。

如果您觉得有所收获,欢迎点“好看”,并且“分享”给需要的人~~