android开发笔记[7]-移植opencv和yolov5

摘要

移植opencv至android;转换yolov5权重为onnx格式并在Android侧利用opencv.dnn(版本:4.6.0)进行推理;android使用opencv测试图片转灰度图.

关键信息

  • Android Studio: Electric Eel | 2022.1.1 Patch 2
  • Gradle:distributionUrl=https://services.gradle.org/distributions/gradle-7.5-bin.zip
  • jvmTarget = '1.8'
  • minSdk 21
  • targetSdk 33
  • compileSdk 33
  • 开发语言:Kotlin,Java
  • ndkVersion = '21.1.6352462'
  • opencv=4.6.0

原理简介

onnx格式

[https://juejin.cn/post/7167762396755263501]
[https://onnx.ai]
ONNX是Open Neural Network Exchange的缩写.
开放神经网络交换(格式),机器学习互操作性的开放标准。

ONNX是2017年9月由微软与Facebook、AWS合作推出的开放的神经网络交换格式。致力于将不同模型转换成统一的ONNX格式,然后再通过统一的方案完成模型部署。
ONNX作为中间层,的一头对接不同的机器学习模型框架,另外一头对接的是不同的编程语言(C++、Java、C#、Python……)、不同OS(windows、Linux……)、不同计算引擎(CPU、CUDA……)的模型部署方案。各种机器学习框架产出的模型,只需要转换成ONNX格式,就自然获得了在多种平台上使用多种编程语言做在线推理的能力。

opencv的dnn推理框架

[https://juejin.cn/post/7283150465524187196]
[https://juejin.cn/post/7098893002713759751]
[https://docs.opencv.org/4.x/d2/d58/tutorial_table_of_content_dnn.html]
[https://docs.opencv.org/4.x/da/d9d/tutorial_dnn_yolo.html]
[https://blog.csdn.net/luyouqi11/article/details/131952082]
OpenCV自3.3版本开始,加入了对深度学习网络的支持,即DNN模块,它支持主流的深度学习框架生成与模型的加载,如onnx模型的加载。

  1. 第一层:语言绑定层,主要支持Python和Java,还包括准确度测试、性能测试和部分示例程序。
  2. 第二层:C++的API层,是原生的API,功能主要包括加载网络模型、推理运算以及获取网络的输出等。
  3. 第三层:实现层,包括模型转换器、DNN引擎以及层实现等。模型转换器将各种网络模型格式转换为DNN模块的内部表示,DNN引擎负责内部网络的组织和优化,层实现指各种层运算的实现过程。
  4. 第四层:加速层,包括CPU加速、GPU加速、Halide加速和Intel推理引擎加速。CPU加速用到了SSE和AVX指令以及大量的多线程元语,而OpenCL加速是针对GPU进行并行运算的加速。Halide是一个实验性的实现,并且性能一般。Intel推理引擎加速需要安装OpenVINO库,它可以实现在CPU、GPU和VPU上的加速,在GPU上内部会调用clDNN库来做GPU上的加速,在CPU上内部会调用MKL-DNN来做CPU加速,而Movidius主要是在VPU上使用的专用库来进行加速。

android的assets资源文件和raw资源文件

[https://www.jianshu.com/p/e27bf552ab64]
在Android中,我们常常提到资源文件一般只要分为两种:

  • assets目录 的文件,被称为原生文件,在apk的编译打包流程不会生成资源ID,也就是说我们无法通过R.xxx.xxx的方式去访问,这个目录下的文件在被打包生成apk的时候不会进行压缩。

  • res目录 的文件,这类文件在打包生成apk的时候,直接通过aapt(资源文件打包工具)打包res资源文件,生成R.java、resources.arsc和res文件,我们可以直接通过R.xxx.xxx的方式访问到资源文件。(注意:raw下的文件会被原封不动的打包到apk中)

  • assets目录 & res/raw目录

先说说raw目录吧。它是创建在res文件下的一个资源文件(常见的会放一些视频/音频文件等等)。它虽然是res的子目录但是它里面的文件在打包生成apk的时候不会像其他res文件一样,对资源文件进行压缩,这一点和assets文件相似。

下面就再说一下assets目录 和 res/raw目录的异同

  • 相同点
    assets和res/raw工程目录下都可以放一些小于1M的文件(2.3版本以前的要求,现在无限制)两个文件夹下的文件都会被原封不动的打包到APK中应用使用。而不会像其它资源文件那样被编译成二进制的形式。(不管放在哪个文件夹下apk的大小是不变的)

  • 不同点
    res/raw中的文件会被映射到R.java文件中,访问的时候直接使用资源ID即R.id.xxx;assets文件夹下的文件不会被映射到R.java中,访问的时候需要AssetManager类。
    res/raw不可以有目录结构,而assets则可以有目录结构,也就是assets目录下可以再建立文件夹.

raw是res的子目录,Android会自动的为这个目录中的所有资源文件生成一个ID并映射到R.java文件中,作为一个文件的引用。这样我们就可以很容易的访问到它了,在Android XML文件中可以用@raw/xxx的形式引用。mp3,mp4等文件适合放在这个目录下。
assets文件更像是一个附录类型的目录,Android不会为这个目录中的资源文件创建ID。同时,访问的时候需要一个字符串路径来获取这个目录下的文件描述,所以访问的速度会更慢。而且我们只能通过AssetManager这个统一管理类来访问这些文件。数据库、字体文件、json文件、html文件适合放在这里.

android的Uri资源引用方式和assets资源引用方式

[https://blog.51cto.com/u_16213653/7378392]
[https://blog.51cto.com/u_16099217/6577093]
[https://blog.csdn.net/qq_32450111/article/details/81704260]
资源是Android应用程序中重要的组成部分。在应用程序中经常会使用字符串、菜单、图像、声音、视频等内容,都可以称之为资源。通过将资源放到与apk文件中与Android应用程序一同发布,在资源文件比较大的情况下,可以通过将资源作为外部文件来使用.
Uri资源通常指的是在res目录下的资源,例如布局资源、图像资源、菜单资源等。这些资源在编译时会自动被打包到apk文件中。
Android将常用的资源统一放置到res目录下。
资源包括:字符串资源、颜色资源、尺寸资源、布局资源、数组资源、图像资源(drawable资源、mipmap资源)、主题和样式资源、菜单资源(选项菜单、上下文菜单).
在代码中,我们可以通过资源的ID来引用Uri资源。例如,如果你有一个名为"image.jpg"的图像资源,它的ID可能是R.drawable.image。那么,你可以在代码中这样引用它:

ImageView imageView = findViewById(R.id.image_view);
imageView.setImageResource(R.drawable.image);

此外,对于res/raw目录下的资源,我们还可以通过Uri的方式来引用。例如,如果你有一个位于res/raw目录下的音频文件"audio.mp3",你可以这样获取其Uri:

Uri audioUri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.audio);

assets资源通常是一些原始的、未经处理的文件,例如文本文件、图片文件、音频文件等。这些文件不会被编译,而是会被原封不动地打包到apk文件中。

在代码中,我们可以通过AssetManager类来访问assets目录中的文件。例如,如果你有一个名为"text.txt"的文本文件,你可以这样获取其输入流:

AssetManager assetManager = getAssets();
InputStream inputStream = assetManager.open("text.txt");

android遍历应用私有目录下文件

// 打印内部存储所有文件
val filesDir = filesDir // 获取内部存储目录
val fileList = filesDir.listFiles() // 获取目录下的所有文件
Log.d("文件", "内部存储所有文件:")
if (fileList != null) {
    for (file in fileList) {
        Log.d("文件", file.name) // 打印文件名
    }
}

实现

核心代码

  1. 转换训练好的yolov5权重到onnx格式(float32精度)
git clone https://github.com/ultralytics/yolov5.git
cd yolov5
# 版本是yolov5.7
python3.11 ./yolov5/export.py --weights ./best.pt --img 640 --batch 1 --include onnx
  1. 移植opencv到android

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.22.1)

# Declares and names the project.

project("grape_yolov5_detect_android")

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

# yolov5葡萄检测
add_library( # Sets the name of the library.
             grape_yolov5_detect_android

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             grape_yolov5_detect_android.cpp )

# opencv测试
add_library(
    grape_opencv_test SHARED grape_opencv_test.cpp
)

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

# 头文件路径
include_directories(./OpenCV/arm64-v8a/sdk/native/jni/include)

# 初始化CMAKE_MODULE_PATH和OpenCV_DIR变量
set(CMAKE_MODULE_PATH "")
set(OpenCV_DIR "")

set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ./OpenCV/arm64-v8a/sdk/native/jni)
set(OpenCV_DIR ${OpenCV_DIR} ./OpenCV/arm64-v8a/sdk/native/jni)

message(WARNING "CMAKE_MODULE_PATH: ${CMAKE_MODULE_PATH}")
message(WARNING "CMAKE_MODULE_PATH: ${OpenCV_DIR}")

find_package(OpenCV REQUIRED)

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log
              android)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
                       grape_yolov5_detect_android
                       android
                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

target_link_libraries(grape_opencv_test android ${log-lib})

target_link_libraries(grape_opencv_test ${OpenCV_LIBS})
target_link_libraries(grape_yolov5_detect_android ${OpenCV_LIBS})

复制预编译库的arm64-v8a文件夹到jni/OpenCV目录;

  • 2.3 配置build.gradle(project)
    (注意:abiFilters 'arm64-v8a')

build.gradle

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    namespace 'cn.qsbye.grape_yolov5_detect_android'
    compileSdk 33

    defaultConfig {
        applicationId "cn.qsbye.grape_yolov5_detect_android"
        minSdk 24
        targetSdk 33
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        externalNativeBuild {
            cmake {
                cppFlags ''
                abiFilters 'arm64-v8a'
            }
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        viewBinding true
    }
    externalNativeBuild {
        cmake {
            path file('src/main/jni/CMakeLists.txt')
            version '3.22.1'
        }
    }
    ndkVersion '21.1.6352462'
}

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.9.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0'
    implementation 'androidx.navigation:navigation-ui-ktx:2.6.0'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
  • 2.4 编辑代码
    grepe_opencv_test.cpp
/*
 * opencv显示图片测试
**/

#include "grape_opencv_test.h"
#include <fstream>
#include <opencv2/opencv.hpp>
#include <jni.h>
#include <android/log.h>
#include <iostream>
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>
#include <opencv2/core/version.hpp>

using namespace cv;

/* start JNI相关 */
extern "C" JNIEXPORT jintArray JNICALL Java_cn_qsbye_grape_1yolov5_1detect_1android_ResultActivity_opencvTest(
        JNIEnv *env, jclass obj, jintArray buf, int w, int h) {

    jint *cbuf;
    cbuf = env->GetIntArrayElements(buf, JNI_FALSE );
    if (cbuf == NULL) {
        return 0;
    }

    Mat imgData(h, w, CV_8UC4, (unsigned char *) cbuf);

    uchar* ptr = imgData.ptr(0);
    for(int i = 0; i < w*h; i ++){
        //计算公式:Y(亮度) = 0.299*R + 0.587*G + 0.114*B
        //对于一个int四字节,其彩色值存储方式为:BGRA
        int grayScale = (int)(ptr[4*i+2]*0.299 + ptr[4*i+1]*0.587 + ptr[4*i+0]*0.114);
        ptr[4*i+1] = grayScale;
        ptr[4*i+2] = grayScale;
        ptr[4*i+0] = grayScale;
    }

    int size = w * h;
    jintArray result = env->NewIntArray(size);
    env->SetIntArrayRegion(result, 0, size, cbuf);
    env->ReleaseIntArrayElements(buf, cbuf, 0);
    __android_log_print(ANDROID_LOG_ERROR, "JNI", "opencvTest() success!");
    return result;
}
/* end JNI相关 */

grape_yolov5_detect_android.cpp

#include <fstream>
#include <opencv2/opencv.hpp>
#include <jni.h>
#include <android/log.h>
#include <iostream>
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>
#include <opencv2/core/version.hpp>

AAssetManager* g_asset_mgr = NULL;

const std::vector<cv::Scalar> colors = { cv::Scalar(255, 255, 0), cv::Scalar(0, 255, 0), cv::Scalar(0, 255, 255), cv::Scalar(255, 0, 0) };

// 输入图片大小, 默认640*640
const float INPUT_WIDTH = 640.0;
const float INPUT_HEIGHT = 640.0;

// 类别得分
const float SCORE_THRESHOLD = 0.5;

// NMS检测
const float NMS_THRESHOLD = 0.5;

// 置信度
const float CONFIDENCE_THRESHOLD = 0.5;

struct Detection{
    int class_id;
    float confidence;
    cv::Rect box;
};

class half;

/**
 * 调整图像大小,使其适应模型输入尺寸
*/
cv::Mat format_yolov5(const cv::Mat& source) {
    __android_log_print(ANDROID_LOG_ERROR, "JNI", "进入format_yolov5函数!");
    int col = source.cols;
    int row = source.rows;
    int _max = MAX(col, row);
    cv::Mat result = cv::Mat::zeros(_max, _max, CV_8UC3);
    source.copyTo(result(cv::Rect(0, 0, col, row)));

//    // 将灰度图像转换为BGR格式
//    cv::cvtColor(source, result, cv::COLOR_GRAY2BGR);

    return result;
}

/**
 * 目标检测函数
*/
void detect(cv::Mat& image, cv::dnn::Net& net, std::vector<Detection>& output, const std::vector<std::string>& className) {
    cv::Mat blob;

    __android_log_print(ANDROID_LOG_ERROR, "JNI", "进入detect函数!");

    auto input_image = format_yolov5(image);

    // 检查转换
    if (input_image.empty()) {
        __android_log_print(ANDROID_LOG_ERROR, "JNI", "input_image转换后为空!");
    }else{
        __android_log_print(ANDROID_LOG_ERROR, "JNI", "input_image转换为yolov5格式成功!");
    }

    // 将输入图像转换为网络所需的blob格式
    cv::dnn::blobFromImage(input_image, blob, 1. / 255., cv::Size(INPUT_WIDTH, INPUT_HEIGHT), cv::Scalar(), true, false);
    net.setInput(blob);

    __android_log_print(ANDROID_LOG_ERROR, "JNI", "将输入图像转换为网络所需的blob格式成功!");

    std::vector<cv::Mat> outputs;
    net.forward(outputs, net.getUnconnectedOutLayersNames());

    float x_factor = input_image.cols / INPUT_WIDTH;
    float y_factor = input_image.rows / INPUT_HEIGHT;

    float* data = (float*)outputs[0].data;

    const int dimensions = 7;
    const int rows = 25200;

    std::vector<int> class_ids;
    std::vector<float> confidences;
    std::vector<cv::Rect> boxes;

    for (int i = 0; i < rows; ++i) {
        float confidence = data[4];
        if (confidence >= CONFIDENCE_THRESHOLD) {
            float* classes_scores = data + 5;
            cv::Mat scores(1, className.size(), CV_32FC1, classes_scores);
            cv::Point class_id;
            double max_class_score;
            minMaxLoc(scores, 0, &max_class_score, 0, &class_id);
            if (max_class_score > SCORE_THRESHOLD) {
                confidences.push_back(confidence);
                class_ids.push_back(class_id.x);

                float x = data[0];
                float y = data[1];
                float w = data[2];
                float h = data[3];
                int left = int((x - 0.5 * w) * x_factor);
                int top = int((y - 0.5 * h) * y_factor);
                int width = int(w * x_factor);
                int height = int(h * y_factor);
                boxes.push_back(cv::Rect(left, top, width, height));
            }
        }
        data += dimensions;
    }

    std::vector<int> nms_result;
    // 应用非极大值抑制获取最终的检测结果
    cv::dnn::NMSBoxes(boxes, confidences, SCORE_THRESHOLD, NMS_THRESHOLD, nms_result);

    for (int i = 0; i < nms_result.size(); i++) {
        int idx = nms_result[i];
        Detection result;
        result.class_id = class_ids[idx];
        result.confidence = confidences[idx];
        result.box = boxes[idx];
        output.push_back(result);
    }
}

/*
 * 葡萄计数
**/
cv::Mat detectGrape(cv::Mat imgdata) {
    __android_log_print(ANDROID_LOG_ERROR, "JNI", "OpenCV version:%s", CV_VERSION);

    /* start 检查imgdata数据 */
    try {
        // 检查imgdata是否为空
        if (imgdata.empty()) {
            __android_log_print(ANDROID_LOG_ERROR, "JNI", "imgdata为空");
            return cv::Mat(); // 返回空的cv::Mat对象
        }
    }catch(...){
        __android_log_print(ANDROID_LOG_ERROR, "JNI", "读取imgdata失败");
        return cv::Mat(); // 返回空的cv::Mat对象
    }

    // 添加打印语句,表示成功读取imgdata
    __android_log_print(ANDROID_LOG_ERROR, "JNI", "读取imgdata成功");

    // 获取imgdata的大小
    size_t imgdataSize = imgdata.total() * imgdata.elemSize();

    // 添加打印语句,表示imgdata的大小
    __android_log_print(ANDROID_LOG_ERROR, "JNI", "大小为:%zu Byte", imgdataSize);

    /* end 检查imgdata数据 */

    /* start 获取assets资源 */

    // 通过assets获取onnx_model文件数据
    std::string model_path = "best_fp32.onnx";
    AAsset *model_asset_file = AAssetManager_open(g_asset_mgr, model_path.c_str(), AASSET_MODE_BUFFER);

    if (model_asset_file == nullptr) {
        __android_log_print(ANDROID_LOG_ERROR, "JNI", "model_asset_file为空指针!");
        while (1);
    }

    size_t model_asset_filelength = AAsset_getLength(model_asset_file);

    char *model_asset_databuffer = (char *) malloc(model_asset_filelength);
    AAsset_read(model_asset_file, model_asset_databuffer, model_asset_filelength);
    AAsset_close(model_asset_file);
    std::vector<char> model_asset_vec(model_asset_databuffer, model_asset_databuffer + model_asset_filelength);
    __android_log_print(ANDROID_LOG_ERROR, "JNI", "读取onnx_model文件成功");
    __android_log_print(ANDROID_LOG_ERROR, "JNI", "onnx_model大小为:%zu Byte", model_asset_filelength);
    /* end 获取assets资源 */

    int cnt_grape = 0;
    std::vector<std::string> class_list = { "grape", "grain" };

    // std::string original_img_path = "G0111.jpg";
    // 随机获取一张图片
    std::string original_img_path = "grape_img/G000" + std::to_string(std::rand() % 10) + ".jpg";
    if(original_img_path == "grape_img/G0000.jpg"){
        original_img_path = "grape_img/G0001.jpg";
    }
    /* start 获取assets资源 */
    // 通过assets获取original_img文件数据
    AAsset *original_img_asset_file = AAssetManager_open(g_asset_mgr, original_img_path.c_str(), AASSET_MODE_BUFFER);

    if (original_img_asset_file == nullptr) {
        __android_log_print(ANDROID_LOG_ERROR, "JNI", "original_img_asset_file:%s为空指针!",original_img_path.c_str());
        while (1) ;
    }

    size_t original_img_asset_filelength = AAsset_getLength(original_img_asset_file);

    char *original_img_asset_databuffer = (char *) malloc(original_img_asset_filelength);
    AAsset_read(original_img_asset_file, original_img_asset_databuffer, original_img_asset_filelength);
    AAsset_close(original_img_asset_file);
    std::vector<char> original_img_asset_vec(original_img_asset_databuffer, original_img_asset_databuffer + original_img_asset_filelength);
    __android_log_print(ANDROID_LOG_ERROR, "JNI", "读取original_img文件成功");
    __android_log_print(ANDROID_LOG_ERROR, "JNI", "original_img大小为:%d Byte",original_img_asset_filelength);
    /* end 获取assets资源 */

    // 使用AAsset方式加载模型数据
    cv::dnn::Net net;
    net = cv::dnn::readNetFromONNX(model_asset_databuffer, model_asset_filelength);
    __android_log_print(ANDROID_LOG_ERROR, "JNI", "加载onnx模型数据成功");

    // 使用AAsset方式加载图片数据
    cv::Mat img_from_assets;
    cv::imdecode(original_img_asset_vec, cv::IMREAD_COLOR, &img_from_assets);
    __android_log_print(ANDROID_LOG_ERROR, "JNI", "img变量大小:%d",img_from_assets.size());

    /* start 预测代码 */
    std::vector<Detection> output;

#if 1
    // 模型预测
    // detect(imgdata, net, output, class_list);
    detect(img_from_assets, net, output, class_list);
//#define imgdata img_from_assets

    int detections = output.size();

    for (int i = 0; i < detections; ++i) {
        auto detection = output[i];
        auto box = detection.box;
        auto classID = detection.class_id;
        const auto color = colors[classID % colors.size()];

        // 类别检测 0:葡萄串;1:果粒
        if (classID == 0) {
            ++cnt_grape;
            cv::rectangle(imgdata, box, color, 3);
        }
        else {
            cv::circle(imgdata, cv::Point(box.x + box.width * 0.5, box.y + box.height * 0.5), 0.25 * (box.height + box.width), color, 3);
        }
        cv::rectangle(imgdata, cv::Point(box.x, box.y - 20), cv::Point(box.x + box.width, box.y), color, cv::FILLED);
        cv::putText(imgdata, class_list[classID].c_str(), cv::Point(box.x, box.y - 5), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0));
    }

    __android_log_print(ANDROID_LOG_ERROR, "JNI", "模型执行预测完成");

    // 绘制左上角的数量统计
    std::string text = std::to_string(detections - cnt_grape);
    cv::Point text_pos(50, 150);
    cv::Scalar text_color(0, 0, 0);
    int font_face = cv::FONT_HERSHEY_SIMPLEX;
    double font_scale = 5.0;
    int thickness = 5;

    cv::putText(imgdata, text, text_pos, font_face, font_scale, text_color, thickness);
    __android_log_print(ANDROID_LOG_ERROR, "JNI", "绘制文字:%s", text.c_str());

    // 返回cv::Mat格式图像数据
    return imgdata;
#endif
}

/* start JNI相关 */
extern "C" JNIEXPORT jintArray JNICALL
Java_cn_qsbye_grape_1yolov5_1detect_1android_ResultActivity_detectGrape(JNIEnv* env, jobject obj, jintArray buf, jint w, jint h) {
    try {
        // 判断buf是否为空
        jint *cbuf;
        cbuf = env->GetIntArrayElements(buf, JNI_FALSE);
        if (cbuf != NULL) {
            // buf不为空
            cv::Mat ori_img_data(h, w, CV_8UC4, (unsigned char *) cbuf);
            cv::Mat result_from_opencv = detectGrape(ori_img_data);
            // 返回数据
            int size = w * h;
            jintArray result_to_return = env->NewIntArray(size);
            env->SetIntArrayRegion(result_to_return, 0, size, reinterpret_cast<jint*>(result_from_opencv.data));
            env->ReleaseIntArrayElements(buf, cbuf, 0);
            __android_log_print(ANDROID_LOG_ERROR, "JNI", "detectGrape() success!");
            return result_to_return;
        }
        else {
            // 如果参数为空,则继续使用原有代码读取assets中的图片
            __android_log_print(ANDROID_LOG_ERROR, "JNI", "参数携带的数据为空,使用默认图片");

            const char* original_img_path = "G0111.jpg";

            /* start 获取assets资源 */
            // 通过assets获取original_img文件数据
            AAsset *original_img_asset_file = AAssetManager_open(g_asset_mgr, original_img_path, AASSET_MODE_BUFFER);

            if (original_img_asset_file == nullptr) {
                __android_log_print(ANDROID_LOG_ERROR, "JNI", "original_img_asset_file为空指针!");
                while (1) ;
            }

            size_t original_img_asset_filelength = AAsset_getLength(original_img_asset_file);

            char *original_img_asset_databuffer = (char *) malloc(original_img_asset_filelength);
            AAsset_read(original_img_asset_file, original_img_asset_databuffer, original_img_asset_filelength);
            AAsset_close(original_img_asset_file);
            std::vector<char> original_img_asset_vec(original_img_asset_databuffer, original_img_asset_databuffer + original_img_asset_filelength);
            __android_log_print(ANDROID_LOG_ERROR, "JNI", "读取original_img文件成功");
            __android_log_print(ANDROID_LOG_ERROR, "JNI", "original_img大小为:%zu Byte", original_img_asset_filelength);
            /* end 获取assets资源 */

            // 将图像数据转换为cv::Mat格式
            cv::Mat img = cv::imdecode(original_img_asset_vec, cv::IMREAD_COLOR);
            __android_log_print(ANDROID_LOG_ERROR, "JNI", "img变量大小:%dx%d", img.cols, img.rows);

            // 调用葡萄检测函数
            cv::Mat result_from_opencv = detectGrape(img);

            // 返回数据
            int size = result_from_opencv.cols * result_from_opencv.rows;
            jintArray result_to_return = env->NewIntArray(size);
            env->SetIntArrayRegion(result_to_return, 0, size, reinterpret_cast<jint*>(result_from_opencv.data));
            __android_log_print(ANDROID_LOG_ERROR, "JNI", "detectGrape() success!");
            return result_to_return;
        }
    } catch (...) {
        __android_log_print(ANDROID_LOG_ERROR, "JNI", "detectGrape() error: unknown exception");
    }
    return NULL;
}

// 获取assets文件夹
extern "C" JNIEXPORT void JNICALL
Java_cn_qsbye_grape_1yolov5_1detect_1android_MainActivity_initAssetManager(JNIEnv* env, jobject obj, jobject assetManager) {
    g_asset_mgr = AAssetManager_fromJava(env, assetManager);
    __android_log_print(ANDROID_LOG_ERROR, "JNI", "MainActivity获取assets实例成功");
    // while(1);
}

// 获取assets文件夹2
extern "C" JNIEXPORT void JNICALL
Java_cn_qsbye_grape_1yolov5_1detect_1android_ResultActivity_initAssetManager(JNIEnv* env, jobject obj, jobject assetManager) {
    g_asset_mgr = AAssetManager_fromJava(env, assetManager);
    __android_log_print(ANDROID_LOG_ERROR, "JNI", "ResultActivity获取assets实例成功");
    // while(1);
}
/* end JNI相关 */
  • 2.5 配置权限
    AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" tools:ignore="ProtectedPermissions" />
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
        tools:ignore="ScopedStorage" />
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/grape_icon_round"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/Theme.Grapeyolov5detectandroid"
        android:requestLegacyExternalStorage="true"
        tools:targetApi="31">

        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.Grapeyolov5detectandroid.NoActionBar">

        </activity>

        <activity
            android:name=".ResultActivity"
            android:exported="true">

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

        </activity>


        <receiver   android:name=".ResultActivity$DetectionCompleteReceiver"
                    android:enabled="true"
                    android:exported="true">
            <intent-filter>
                <action android:name="cn.qsbye.grape_yolov5_detect_android.DETECTION_COMPLETE"/>
            </intent-filter>
        </receiver>

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="cn.qsbye.grape_yolov5_detect_android.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

    </application>

</manifest>
  • 2.6 编辑kotlin代码
    MainActivity.kt
package cn.qsbye.grape_yolov5_detect_android

import android.content.Context
import android.content.Intent
import android.content.res.AssetManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Button
import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import cn.qsbye.grape_yolov5_detect_android.databinding.ActivityMainBinding
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import kotlin.random.Random


class MainActivity : AppCompatActivity() {

    private lateinit var appBarConfiguration: AppBarConfiguration
    private lateinit var binding: ActivityMainBinding

    companion object {
        @JvmStatic
        external fun initAssetManager(assetManager: AssetManager)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 初始化assets文件夹实例
        val assetManager: AssetManager = assets
        initAssetManager(assetManager)

        // 监听幸运按钮
        val luckyButton = findViewById<Button>(R.id.lucky_btn)
        luckyButton.setOnClickListener {
            val s_grape_file_str = String.format("grape_img/G%04d.jpg", Random.nextInt(1, 10))

            try {
                // 从assets目录中读取位图数据
                val inputStream = assets.open(s_grape_file_str)
                val bitmap = BitmapFactory.decodeStream(inputStream)

                Log.e("检测", "当前识别的图片为:$s_grape_file_str")

                // 将位图保存到应用的缓存目录中
                val file = File(cacheDir, "grape_image.jpg")
                val outputStream = FileOutputStream(file)
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
                outputStream.flush()
                outputStream.close()

                // 创建一个 Intent 对象,指定当前活动(this)和目标活动(ResultActivity::class.java)
                val intent = Intent(this@MainActivity, ResultActivity::class.java)

                // 将位图的 URI 作为附加参数传递给 ResultActivity
                val imageUri = FileProvider.getUriForFile(this, "cn.qsbye.grape_yolov5_detect_android.fileprovider", file)
                intent.putExtra("original_grape_bitmap_uri", imageUri)

                // 启动 ResultActivity
                startActivity(intent)

                // 不会执行:在解析位图完成后发送广播通知 ResultActivity
                val broadcastIntent = Intent("cn.qsbye.grape_yolov5_detect_android.DETECTION_COMPLETE")
                sendBroadcast(broadcastIntent)
            } catch (e: IOException) {
                e.printStackTrace()
            }

        } // end setOnClickListener

    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        // Inflate the menu; this adds items to the action bar if it is present.
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        return when (item.itemId) {
            R.id.action_settings -> true
            else -> super.onOptionsItemSelected(item)
        }
    }

    override fun onSupportNavigateUp(): Boolean {
        val navController = findNavController(R.id.nav_host_fragment_content_main)
        return navController.navigateUp(appBarConfiguration)
                || super.onSupportNavigateUp()
    }
}

ResultActivity.kt

package cn.qsbye.grape_yolov5_detect_android

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.AssetManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.widget.ProgressBar
import java.io.BufferedReader
import java.io.InputStreamReader

class ResultActivity : AppCompatActivity() {

    private val detectionCompleteReceiver = DetectionCompleteReceiver()

    // JNI函数:opencv测试
    external fun opencvTest(bitmapbuf:IntArray,w:Int,h:Int):IntArray
    // JNI函数:葡萄计数
    external fun detectGrape(bitmapbuf:IntArray,w:Int,h:Int):IntArray

    companion object {
        init {
            System.loadLibrary("grape_opencv_test")
            System.loadLibrary("grape_yolov5_detect_android")
        }

        @JvmStatic
        external fun initAssetManager(assetManager: AssetManager)
    }

    // 监听广播
    inner class DetectionCompleteReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            // 监听到广播后设置 progress_bar 的可见性
            runOnUiThread {
                Log.e("检测", "接收到广播通知")
                val progressBar = findViewById<ProgressBar>(R.id.progress_bar)
                progressBar.visibility = View.INVISIBLE
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_result)

        // 初始化assets文件夹实例
        val assetManager: AssetManager = assets
        initAssetManager(assetManager)

        // 注册广播接收器
        val filter = IntentFilter("cn.qsbye.grape_yolov5_detect_android.DETECTION_COMPLETE")
        registerReceiver(detectionCompleteReceiver, filter)

        // 延时5s后自动隐藏
        val handler = Handler()
        val delayMillis = 5000 // 5秒

        handler.postDelayed({
            runOnUiThread {
                val progressBar = findViewById<ProgressBar>(R.id.progress_bar)
                progressBar.visibility = View.INVISIBLE
            }
        }, delayMillis.toLong())

        // 监听@+id/return_btn然后返回MainActivity
        val returnButton = findViewById<Button>(R.id.return_btn)
        returnButton.setOnClickListener {
            // 创建一个 Intent 对象,指定当前活动(this)和目标活动(MainActivity::class.java)
            val intent = Intent(this, MainActivity::class.java)

            // 启动 MainActivity
            startActivity(intent)

            // 结束当前的 ResultActivity
            finish()
        } // end setOnClickListener

        /* start 显示原图 */
        // 获取传递的参数
        val imageUri = intent.getParcelableExtra<Uri>("original_grape_bitmap_uri")

        // 判断imageUri是否为空
        val bitmap: Bitmap = if (imageUri == null) {
            // 如果imageUri为空,则使用assets/"G0111.jpg"图片
            val inputStream = assets.open("G0111.jpg")
            BitmapFactory.decodeStream(inputStream)
        } else {
            // 通过 URI 加载位图
            BitmapFactory.decodeStream(contentResolver.openInputStream(imageUri))
        }

        // 显示图片
        val imageView = findViewById<ImageView>(R.id.original_image)
        imageView.setImageBitmap(bitmap)
        /* end 显示原图 */

        /* start 显示结果图 */
        val processedImageView = findViewById<ImageView>(R.id.processed_image)
        val pixels = IntArray(bitmap.width * bitmap.height)
        bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
        val resultIntArray = detectGrape(pixels, bitmap.width, bitmap.height)
        // val resultIntArray = opencvTest(pixels, bitmap.width, bitmap.height)
        val resultBitmap = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
        resultBitmap.setPixels(resultIntArray, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
        processedImageView.setImageBitmap(resultBitmap)
        /* end 显示结果图 */

        /* start opencv测试 */
        val bitmap2 = (resources.getDrawable(R.drawable.g0033) as BitmapDrawable).bitmap
        val original_img = findViewById<ImageView>(R.id.original_image)
        original_img.setImageBitmap(bitmap)
        val w = bitmap2.width
        val h = bitmap2.height
        val pix = IntArray(w * h)
        bitmap2.getPixels(pix, 0, w, 0, 0, w, h)
        val resultPixes = opencvTest(pix, w, h)
        val result = Bitmap.createBitmap(w, h, Bitmap.Config.RGB_565)
        result.setPixels(resultPixes, 0, w, 0, 0, w, h)

        val img = findViewById<ImageView>(R.id.processed_image)
        img.setImageBitmap(result)
        /* end opencv测试 */

        // 打印内部存储所有文件
        val filesDir = filesDir // 获取内部存储目录
        val fileList = filesDir.listFiles() // 获取目录下的所有文件
        Log.d("文件", "内部存储所有文件:")
        if (fileList != null) {
            for (file in fileList) {
                Log.d("文件", file.name) // 打印文件名
            }
        }


    }// end onCreate

    override fun onDestroy() {
        super.onDestroy()
        // 注销广播接收器
        unregisterReceiver(detectionCompleteReceiver)
    }

}
  • 2.7 新建assets目录
    src/main下新建assets文件夹,放置best_fp32.onnx资源文件和需要处理的图片如G0111.jpg和图片文件夹grape_img;

  • 2.8 编译运行
    输出:

E/检测: 当前识别的图片为:grape_img/G0003.jpg
I/TopResumedActivityChangeItem: execute start, ActivityClientRecord = ActivityRecord{537ff25 token=android.os.BinderProxy@40381d7 {cn.qsbye.grape_yolov5_detect_android/cn.qsbye.grape_yolov5_detect_android.MainActivity}}
I/HwViewRootImpl: Add sceneId 2 topId: 0
I/HwViewRootImpl: Add sceneId 9 topId: 2
I/LaunchActivityItem: execute start, token = android.os.BinderProxy@c892f80
V/ActivityThread: callActivityOnCreate
I/DecorView[]: pkgName:cn.qsbye.grape_yolov5_detect_android old windowMode:0 new windoMode:1, isFixedSize:false, isStackNeedCaptionView:true
E/JNI: ResultActivity获取assets实例成功
E/BitmapFactory: doDecode: javaHwBitmap is null
E/JNI: OpenCV version:4.6.0
E/JNI: 读取imgdata成功
E/JNI: 大小为:47235072 Byte
E/JNI: 读取onnx_model文件成功
E/JNI: onnx_model大小为:28509804 Byte
E/JNI: 读取original_img文件成功
E/JNI: original_img大小为:2737882 Byte
E/JNI: 加载onnx模型数据成功
E/JNI: img变量大小:2976
E/JNI: 进入detect函数!
E/JNI: 进入format_yolov5函数!
E/JNI: input_image转换为yolov5格式成功!
E/JNI: 将输入图像转换为网络所需的blob格式成功!
E/JNI: 模型执行预测完成
E/JNI: 绘制文字:48
E/JNI: detectGrape() success!
D/文件: 内部存储所有文件:
D/文件: profileInstalled
D/文件: original_img.jpg
D/文件: hw_cached_resid.list

效果

opencv灰度图测试 opencv推理测试
posted @   qsBye  阅读(53)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
历史上的今天:
2023-01-30 使用Blade-build编译小记
点击右上角即可分享
微信分享提示