pyTorch入门(六)——实战Android Minist OpenCV手写数字识别(附源码地址)

news2024/10/9 12:24:15

学更好的别人,

做更好的自己。

——《微卡智享》

5288417199d3d0462ec953ee7fe2d1f1.jpeg

本文长度为4239,预计阅读12分钟

前言

前面几篇文章实现了pyTorch训练模型,然后在Windows平台用C++ OpenCV DNN推理都实现了,这篇就来看看在Android端直接实现一个手写数字识别的功能。本篇最后会放出源码地址。

5892ca4301a26b5a3e96b1c25ed35d0a.png

实现效果

78f00345e0bb76d8f1729474ae87a942.gif

bdef909f4a27cb26c5eb67b9243ef29a.png

代码实现

e1d99b1d41069a8c9ba591caa24a53a3.png

微卡智享

实现Android端后写数字识别,一个是项目的OpenCV的环境搭建,详细的搭建可以看《OpenCV4Android中NDK开发(一)--- OpenCV4.1.0环境搭建》,这里只做一下简单介绍了。另一个就是手写板的实现,手写板在前面的《Android Kotlin制作签名白板并保存图片》中已经完成,这次直接将里面现成的类拿过来用即可。

01

项目配置

创建的项目是Native C++的项目,所以cpp文件夹这些都已经创建好了。OpenCV是从官网直接下载的Andorid版本,用的是最新的4.6版本

abe9925d3e9d296c35d2f13efe29a722.png

下载好的OpenCV4.6 Android SDK

d46d4568645b3fbd847d4450888fbc40.png

将里面动态库拷贝到项目目录下的libs下,这里我只拷了3个CPU架构的,因为用虚拟机,所以加上了x86

c1ca6011b96604f7c3d246040ed89ef2.png

76c8322873082025b00bd3c180898d8e.png

cf10aa9fc8285579d5bbcf5a9f124f45.png

然后将OpenCV Android SDK里面的OpenCV头文件复制到程序目录的cpp文件夹下

配置CMakeLists

# 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.18.1)


# Declares and names the project.


project("opencvminist4android")


#定义变量opencvlibs使后面的命令可以使用定位具体的库文件
set(opencvlibs ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs)


#调用头文件的具体路径
include_directories(${CMAKE_CURRENT_SOURCE_DIR})


#增加OpenCV的动态库
add_library(libopencv_java4 SHARED IMPORTED)


#建立链接
set_target_properties(libopencv_java4 PROPERTIES IMPORTED_LOCATION
        "${opencvlibs}/${ANDROID_ABI}/libopencv_java4.so")




# 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.


file(GLOB native_srcs "*.cpp")


add_library( # Sets the name of the library.
        opencvminist4android


        # Sets the library as a shared library.
        SHARED


        # Provides a relative path to your source file(s).
        ${native_srcs})


# 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.


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)


# 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.
        opencvminist4android
        jnigraphics
        libopencv_java4


        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})

c0a05736391d497daea5c2f2ff9b5e5a.png

build.gradle中要加入相关的配置

b23bbdc6cf8badd9b66fa692a0261407.png

02

C++中的代码处理

6c31157ef3c673a45554308ba48a6c0f.png

图中看到native-lib.cpp是JNI中的入口,而这里创建了两个C++的类imgUtil和dnnUtil,一个是图像的处理,一个是DNN推理用的类。

imgUtil类

6ec57e4e1b5a49ba5376c43f3129d65a.png

几个函数中下面的sortRect和dealInputMat这两个函数就是前面章里面用到的函数,这里将他们放到这个类里面了。而Android中保存的bitmap图像在OpenCV中需要进行转换处理,所以上面的三个函数是bitmap和Mat之间相互转换用的。

#include "imgUtil.h"


//Bitmap转为Mat
Mat imgUtil::bitmap2Mat(JNIEnv *env, jobject bmp) {


    Mat src;
    AndroidBitmapInfo bitmapInfo;
    void *pixelscolor;
    int ret;
    try {
        //获取图像信息,如果返回值小于0就是执行失败
        if ((ret = AndroidBitmap_getInfo(env, bmp, &bitmapInfo)) < 0) {
            LOGI("AndroidBitmap_getInfo failed! error-%d", ret);
            return src;
        }


        //判断图像类型是不是RGBA_8888类型
        if (bitmapInfo.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
            LOGI("BitmapInfoFormat error");
            return src;
        }


        //获取图像像素值
        if ((ret = AndroidBitmap_lockPixels(env, bmp, &pixelscolor)) < 0) {
            LOGI("AndroidBitmap_lockPixels() failed ! error=%d", ret);
            return src;
        }


        //生成源图像
        src = Mat(bitmapInfo.height, bitmapInfo.width, CV_8UC4, pixelscolor);


        return src;
    } catch (Exception e) {
        jclass je = env->FindClass("java/lang/Exception");
        env->ThrowNew(je, e.what());
        return src;
    } catch (...) {
        jclass je = env->FindClass("java/lang/Exception");
        env->ThrowNew(je, "Unknown exception in JNI code {bitmap2Mat}");
        return src;
    }
}


//获取Bitmap的参数
jobject imgUtil::getBitmapConfig(JNIEnv *env, jobject bmp) {
    //获取原图片的参数
    jclass java_bitmap_class = (jclass) env->FindClass("android/graphics/Bitmap");
    jmethodID mid = env->GetMethodID(java_bitmap_class, "getConfig",
                                     "()Landroid/graphics/Bitmap$Config;");
    jobject bitmap_config = env->CallObjectMethod(bmp, mid);
    return bitmap_config;
}


//Mat转为Bitmap
jobject
imgUtil::mat2Bitmap(JNIEnv *env, Mat &src, bool needPremultiplyAlpha, jobject bitmap_config) {


    jclass java_bitmap_class = (jclass) env->FindClass("android/graphics/Bitmap");
    jmethodID mid = env->GetStaticMethodID(java_bitmap_class, "createBitmap",
                                           "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
    jobject bitmap = env->CallStaticObjectMethod(java_bitmap_class,
                                                 mid, src.size().width, src.size().height,
                                                 bitmap_config);
    AndroidBitmapInfo info;
    void *pixels = 0;


    try {
        CV_Assert(AndroidBitmap_getInfo(env, bitmap, &info) >= 0);
        CV_Assert(src.type() == CV_8UC1 || src.type() == CV_8UC3 || src.type() == CV_8UC4);
        CV_Assert(AndroidBitmap_lockPixels(env, bitmap, &pixels) >= 0);
        CV_Assert(pixels);


        if (info.format == ANDROID_BITMAP_FORMAT_RGBA_8888) {
            cv::Mat tmp(info.height, info.width, CV_8UC4, pixels);
            if (src.type() == CV_8UC1) {
                cvtColor(src, tmp, cv::COLOR_GRAY2RGBA);
            } else if (src.type() == CV_8UC3) {
                cvtColor(src, tmp, cv::COLOR_RGB2BGRA);
            } else if (src.type() == CV_8UC4) {
                if (needPremultiplyAlpha) {
                    cvtColor(src, tmp, cv::COLOR_RGBA2mRGBA);
                } else {
                    src.copyTo(tmp);
                }
            }
        } else {
            // info.format == ANDROID_BITMAP_FORMAT_RGB_565
            cv::Mat tmp(info.height, info.width, CV_8UC2, pixels);
            if (src.type() == CV_8UC1) {
                cvtColor(src, tmp, cv::COLOR_GRAY2BGR565);
            } else if (src.type() == CV_8UC3) {
                cvtColor(src, tmp, cv::COLOR_RGB2BGR565);
            } else if (src.type() == CV_8UC4) {
                cvtColor(src, tmp, cv::COLOR_RGBA2BGR565);
            }
        }
        AndroidBitmap_unlockPixels(env, bitmap);
        return bitmap;
    } catch (Exception e) {
        AndroidBitmap_unlockPixels(env, bitmap);
        jclass je = env->FindClass("java/lang/Exception");
        env->ThrowNew(je, e.what());
        return bitmap;
    } catch (...) {
        AndroidBitmap_unlockPixels(env, bitmap);
        jclass je = env->FindClass("java/lang/Exception");
        env->ThrowNew(je, "Unknown exception in JNI code {nMatToBitmap}");
        return bitmap;
    }
}


//排序矩形
void imgUtil::sortRect(vector<Rect> &inputrects) {
    for (int i = 0; i < inputrects.size(); ++i) {
        for (int j = i; j < inputrects.size(); ++j) {
            //说明顺序在上方,这里不用变
            if (inputrects[i].y + inputrects[i].height < inputrects[i].y) {


            }
                //同一排
            else if (inputrects[i].y <= inputrects[j].y + inputrects[j].height) {
                if (inputrects[i].x > inputrects[j].x) {
                    swap(inputrects[i], inputrects[j]);
                }
            }
                //下一排
            else if (inputrects[i].y > inputrects[j].y + inputrects[j].height) {
                swap(inputrects[i], inputrects[j]);
            }
        }
    }
}


//处理DNN检测的MINIST图像,防止长方形图像直接转为28*28扁了
void imgUtil::dealInputMat(Mat &src, int row, int col, int tmppadding) {
    int w = src.cols;
    int h = src.rows;
    //看图像的宽高对比,进行处理,先用padding填充黑色,保证图像接近正方形,这样缩放28*28比例不会失衡
    if (w > h) {
        int tmptopbottompadding = (w - h) / 2 + tmppadding;
        copyMakeBorder(src, src, tmptopbottompadding, tmptopbottompadding, tmppadding, tmppadding,
                       BORDER_CONSTANT, Scalar(0));
    }
    else {
        int tmpleftrightpadding = (h - w) / 2 + tmppadding;
        copyMakeBorder(src, src, tmppadding, tmppadding, tmpleftrightpadding, tmpleftrightpadding,
                       BORDER_CONSTANT, Scalar(0));


    }
    resize(src, src, Size(row, col));
}

dnnUtil类

2af01822b73ed7284d83589d2c51ff2f.png

Dnn推理类中,只有两个函数,一个是初始化,也就是加载模型,需要读取本地的模型文件加载进来。另一个就是推理的函数。

关于模型文件

345d2256f1a3a771366f9957b25b2abc.png

上图中可以看到,模型文件选择我们在训练中识别率最高的ResNet的模型,将模型文件直接复制进了raw资源下,注意原来创建时文件名有大写,在这里面要全部改为小写。在Android端程序启动的时候先读取资源文件,再将模型拷贝到本地,把路径通过JNI传递到C++里面,初始化即可。

#include "dnnUtil.h"


bool dnnUtil::InitDnnNet(string onnxdesc) {
    _onnxdesc = onnxdesc;


    _net = dnn::readNetFromONNX(_onnxdesc);
    _net.setPreferableTarget(dnn::DNN_TARGET_CPU);


    return !_net.empty();
}


Mat dnnUtil::DnnPredict(Mat src) {
    Mat inputBlob = dnn::blobFromImage(src, 1, Size(28, 28), Scalar(), false, false);


    //输入参数值
    _net.setInput(inputBlob, "input");
    //预测结果
    Mat output = _net.forward("output");


    return output;
}

JNI入口及native-lib.cpp

9bf057af7a2b9ed894a9c40893458921.png

在Android端创建了一个OpenCVJNI的类,入口的函数写了4个,一个初始化DNN,两个识别的函数,还有一个测试用的。

上面说的将资源文件读取拷贝出来,再进行DNN的初始化就是initOnnxModel这个函数实现的,代码如下:

fun initOnnxModel(context: Context, rawid: Int): Boolean {
        try {
            val onnxDir: File = File(context.filesDir, "onnx")
            if (!onnxDir.exists()) {
                onnxDir.mkdirs()
            }
            //判断模型是否存在是否存在,不存在复制过来
            val onnxfile: File = File(onnxDir, "dnnNet.onnx")
            if (onnxfile.exists()){
                return initOpenCVDNN(onnxfile.absolutePath)
            }else {
                // load cascade file from application resources
                val inputStream = context.resources.openRawResource(rawid)


                val os: FileOutputStream = FileOutputStream(onnxfile)
                val buffer = ByteArray(4096)
                var bytesRead: Int
                while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                    os.write(buffer, 0, bytesRead)
                }
                inputStream.close()
                os.close()
                return initOpenCVDNN(onnxfile.absolutePath)
            }
        } catch (e: Exception) {
            e.printStackTrace()
            return false
        }
    }

external对应到native-lib.cpp中,即下面的源码

#pragma once


#include <jni.h>
#include <string>
#include <android/log.h>
#include <opencv2/opencv.hpp>
#include "dnnUtil.h"
#include "imgUtil.h"


#define LOG_TAG "System.out"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)


using namespace cv;
using namespace std;


dnnUtil _dnnUtil;
imgUtil _imgUtil = imgUtil();


extern "C"
JNIEXPORT jboolean JNICALL
Java_dem_vaccae_opencvminist4android_OpenCVJNI_initOpenCVDNN(JNIEnv *env, jobject thiz,
                                                             jstring onnxfilepath) {
    try {
        string onnxfile = env->GetStringUTFChars(onnxfilepath, 0);
        //初始化DNN
        _dnnUtil = dnnUtil();
        jboolean res = _dnnUtil.InitDnnNet(onnxfile);


        return res;
    } catch (Exception e) {
        jclass je = env->FindClass("java/lang/Exception");
        env->ThrowNew(je, e.what());
    } catch (...) {
        jclass je = env->FindClass("java/lang/Exception");
        env->ThrowNew(je, "Unknown exception in JNI code {initOpenCVDNN}");
    }
}
extern "C"
JNIEXPORT jobject JNICALL
Java_dem_vaccae_opencvminist4android_OpenCVJNI_ministDetector(JNIEnv *env, jobject thiz,
                                                              jobject bmp) {
    try {
        jobject bitmapcofig = _imgUtil.getBitmapConfig(env, bmp);


        string resstr = "";


        Mat src = _imgUtil.bitmap2Mat(env, bmp);
        //备份源图
        Mat backsrc;
        //将备份的图片从BGRA转为RGB,防止颜色不对
        cvtColor(src, backsrc, COLOR_BGRA2RGB);


        cvtColor(src, src, COLOR_BGRA2GRAY);
        GaussianBlur(src, src, Size(3, 3), 0.5, 0.5);
        //二值化图片,注意用THRESH_BINARY_INV改为黑底白字,对应MINIST
        threshold(src, src, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);


        //做彭账处理,防止手写的数字没有连起来,这里做了3次膨胀处理
        Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
        //加入开运算先去燥点
        morphologyEx(src, src, MORPH_OPEN, kernel, Point(-1, -1));
        morphologyEx(src, src, MORPH_DILATE, kernel, Point(-1, -1), 3);


        vector<vector<Point>> contours;
        vector<Vec4i> hierarchy;
        vector<Rect> rects;


        //查找轮廓
        findContours(src, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE);
        for (int i = 0; i < contours.size(); ++i) {
            RotatedRect rect = minAreaRect(contours[i]);
            Rect outrect = rect.boundingRect();
            //插入到矩形列表中
            rects.push_back(outrect);
        }


        //按从左到右,从上到下排序
        _imgUtil.sortRect(rects);
        //要输出的图像参数
        for (int i = 0; i < rects.size(); ++i) {
            Mat tmpsrc = src(rects[i]);
            _imgUtil.dealInputMat(tmpsrc);
            //预测结果
            Mat output = _dnnUtil.DnnPredict(tmpsrc);


            //查找出结果中推理的最大值
            Point maxLoc;
            minMaxLoc(output, NULL, NULL, NULL, &maxLoc);


            //返回字符串值
            resstr += to_string(maxLoc.x);


            //画出截取图像位置,并显示识别的数字
            rectangle(backsrc, rects[i], Scalar(0, 0, 255), 5);
            putText(backsrc, to_string(maxLoc.x), Point(rects[i].x, rects[i].y), FONT_HERSHEY_PLAIN,
                    5, Scalar(0, 0, 255), 5, -1);


        }


        jobject resbmp = _imgUtil.mat2Bitmap(env, backsrc, false, bitmapcofig);


        //获取MinistResult返回类
        jclass ministresultcls = env->FindClass("dem/vaccae/opencvminist4android/MinistResult");
        //定义MinistResult返回类属性
        jfieldID ministmsg = env->GetFieldID(ministresultcls, "msg", "Ljava/lang/String;");
        jfieldID ministbmp = env->GetFieldID(ministresultcls, "bmp", "Landroid/graphics/Bitmap;");


        //创建返回类
        jobject ministresultobj = env->AllocObject(ministresultcls);
        //设置返回消息
        env->SetObjectField(ministresultobj, ministmsg, env->NewStringUTF(resstr.c_str()));
        //设置返回的图片信息
        env->SetObjectField(ministresultobj, ministbmp, resbmp);




        AndroidBitmap_unlockPixels(env, bmp);


        return ministresultobj;
    } catch (Exception e) {
        jclass je = env->FindClass("java/lang/Exception");
        env->ThrowNew(je, e.what());
    } catch (...) {
        jclass je = env->FindClass("java/lang/Exception");
        env->ThrowNew(je, "Unknown exception in JNI code {bitmap2Mat}");
    }
}






extern "C"
JNIEXPORT jobject JNICALL
Java_dem_vaccae_opencvminist4android_OpenCVJNI_thresholdBitmap(JNIEnv *env, jobject thiz,
                                                               jobject bmp) {
    try {
        jobject bitmapcofig = _imgUtil.getBitmapConfig(env, bmp);


        Mat src = _imgUtil.bitmap2Mat(env, bmp);
        cvtColor(src, src, COLOR_BGRA2GRAY);
        threshold(src, src, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);


        jobject resbmp = _imgUtil.mat2Bitmap(env, src, false, bitmapcofig);


        AndroidBitmap_unlockPixels(env, bmp);


        return resbmp;
    } catch (Exception e) {
        jclass je = env->FindClass("java/lang/Exception");
        env->ThrowNew(je, e.what());
    } catch (...) {
        jclass je = env->FindClass("java/lang/Exception");
        env->ThrowNew(je, "Unknown exception in JNI code {bitmap2Mat}");
    }
}
extern "C"
JNIEXPORT jstring JNICALL
Java_dem_vaccae_opencvminist4android_OpenCVJNI_ministDetectorText(JNIEnv *env, jobject thiz,
                                                                  jobject bmp) {
    try {
        string resstr = "";


        //获取图像转为Mat
        Mat src = _imgUtil.bitmap2Mat(env, bmp);
        //备份源图
        Mat backsrc, dst;
        //备份用于绘制图像,防止颜色有问题,将BGRA转为RGB
        cvtColor(src, dst, COLOR_BGRA2RGB);
        //灰度图,处理的图像
        cvtColor(src, backsrc, COLOR_BGRA2GRAY);
        GaussianBlur(backsrc, backsrc, Size(3, 3), 0.5, 0.5);
        //二值化图片,注意用THRESH_BINARY_INV改为黑底白字,对应MINIST
        threshold(backsrc, backsrc, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);


        //做彭账处理,防止手写的数字没有连起来,这里做了3次膨胀处理
        Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
        //加入开运算先去燥点
        morphologyEx(backsrc, backsrc, MORPH_OPEN, kernel, Point(-1, -1));
        morphologyEx(backsrc, backsrc, MORPH_DILATE, kernel, Point(-1, -1), 3);


        vector<vector<Point>> contours;
        vector<Vec4i> hierarchy;
        vector<Rect> rects;


        //查找轮廓
        findContours(backsrc, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE);
        for (int i = 0; i < contours.size(); ++i) {
            RotatedRect rect = minAreaRect(contours[i]);
            Rect outrect = rect.boundingRect();
            //插入到矩形列表中
            rects.push_back(outrect);
        }


        //按从左到右,从上到下排序
        _imgUtil.sortRect(rects);
        //要输出的图像参数
        for (int i = 0; i < rects.size(); ++i) {
            Mat tmpsrc = backsrc(rects[i]);
            _imgUtil.dealInputMat(tmpsrc);
            //预测结果
            Mat output = _dnnUtil.DnnPredict(tmpsrc);


            //查找出结果中推理的最大值
            Point maxLoc;
            minMaxLoc(output, NULL, NULL, NULL, &maxLoc);


            //返回字符串值
            resstr += to_string(maxLoc.x);


            //画出截取图像位置,并显示识别的数字
            rectangle(dst, rects[i], Scalar(0, 0, 255), 5);
            putText(dst, to_string(maxLoc.x), Point(rects[i].x, rects[i].y), FONT_HERSHEY_PLAIN,
                    5, Scalar(0, 0, 255), 5, -1);


        }


        //用RGB处理完后的图像,需要转为BGRA再覆盖原来的SRC,这样直接就可以修改源图了
        cvtColor(dst, dst, COLOR_RGB2BGRA);
        dst.copyTo(src);


        AndroidBitmap_unlockPixels(env, bmp);


        return env->NewStringUTF(resstr.c_str());
    } catch (Exception e) {
        jclass je = env->FindClass("java/lang/Exception");
        env->ThrowNew(je, e.what());
    } catch (...) {
        jclass je = env->FindClass("java/lang/Exception");
        env->ThrowNew(je, "Unknown exception in JNI code {bitmap2Mat}");
    }
}

03

Android代码

28d794e323309f6b5b33c616254989ed.png

SignatureView是手写板的类,直接从原来那个Demo中拷贝过来了

9a17fbb522b7f5704d9deb2cd9a50dbe.png

MinistResult类只有两个属性,一个String和一个Bitmap,就是返回的处理后图像和识别的字符串。其实可以直接在原来的Bitmap中修改图像显示,不需要返回类了,那个在JNI中也有实现,只不过既然是练习Demo,就多掌握点知识,直接在NDK中实现返回类的效果

MainActivity中代码,主要是实现手写即显示的效果,这里直接贴上代码:

package dem.vaccae.opencvminist4android


import android.Manifest
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Color
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.createBitmap
import dem.vaccae.opencvminist4android.databinding.ActivityMainBinding
import java.io.File


class MainActivity : AppCompatActivity() {


    private lateinit var binding: ActivityMainBinding
    private var isInitDNN: Boolean = false


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)


        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)


        //初始化DNN
        isInitDNN = try {
            val jni = OpenCVJNI()
            val res = jni.initOnnxModel(this, R.raw.resnet)
            binding.tvshow.text = if(res){
                "OpenCV DNN初始化成功"
            }else{
                "OpenCV DNN初始化失败"
            }
            res
        } catch (e: Exception) {
            binding.tvshow.text = e.message
            false
        }


        binding.signatureView.setBackgroundColor(Color.rgb(245, 245, 245))


        binding.btnclear.setOnClickListener {
            binding.signatureView.clear()
        }


        binding.btnSave.setOnClickListener {
            if(!isInitDNN) return@setOnClickListener
            val bmp = binding.signatureView.getBitmapFromView()
            //处理图像
            val ministres:MinistResult? = try{
                val jni = OpenCVJNI()
                jni.ministDetector(bmp)
            }catch (e:Exception){
                binding.tvshow.text = e.message
                null
            }


            ministres?.let {
                binding.tvshow.text = it.msg
                binding.imgv.scaleType = ImageView.ScaleType.FIT_XY
                binding.imgv.setImageBitmap(it.bmp)
            }


//            val strres = try{
//                val jni = OpenCVJNI()
//                jni.ministDetectorText(bmp)
//            }catch (e:Exception){
//                binding.tvshow.text = e.message
//                null
//            }
//
//            strres?.let {
//                binding.tvshow.text = it
//                binding.imgv.scaleType = ImageView.ScaleType.FIT_XY
//                binding.imgv.setImageBitmap(bmp)
//            }
        }




    }


}

微卡智享

划重点

关于NDK中返回类

061ae878aabfeb2c07624b3238f5832c.png

上面的JNI即返回的是MinistResult的类,在NDK中就需要进行处理了,如下图:

695112c6bad4ff887bc7af13a5114c82.png

关于Bitmap到NDK中Mat的处理

d5c95ba5ffd7bb52637b0434e7064252.png

将Bitmap转为Mat,图像的类型是RGBA_8888,所以生成的Mat是8UC4,而在做图像处理的时候,OpenCV的RGB是倒过来的,即BGR,所以cvtColor时,要从BGRA进行转换,如下图:

51825b868152ffb0d670e547cfbfcbd3.png

这里做了两次转换,dst从BGRA转为RGB,是用于标记出轮廓的框和识别的数字标识,如果这里不转为RGB,标出的轮廓框和字符的颜色有问题。

而backsrc中从BGRA转为GRAY灰度图,则是进行图像的正常处理了。

938467a091a2bf4b3cf4aeb22333ebbb.png

而处理完的dst图像需要先从RGB转换为BGRA,然后再通过CopyTo赋值给src,因为Src地址才是指向我们传入的bitmap,只有修改了src,原来的bitmap才会进行修改。处理完src后,需要再通过AndroidBitmap_unlockPixels供Android端继续使用

这样一个Android端的手写数字识别的Demo就完成了,文章只是说了一些重点的地方,具体的实现可以通过下载源码运行看看。源码中包括了pyTorch的训练,VS中C++ OpenCV的推理及生成训练图片,及我们现在这个Android的手写数字识别的完整Demo

311b4e1fe6e27a57095392cabca30b87.png

76aedd4a8d4a69dde1cb060f110a06b2.png

微卡智享

源码地址

https://github.com/Vaccae/pyTorchMinistLearn.git

点击阅读原文可以看到“码云”的代码地址

924e0cdbf8d0bb73e465b5ceba8584cd.png

de7eea0e67d5fa42773bf93c4fbc53ec.png

往期精彩回顾

 

535c341c3743976e2853396daf160b6a.jpeg

pyTorch入门(五)——训练自己的数据集

 

 

ad59ca3e6059e4d88473ff9ef8d9284a.jpeg

pyTorch入门(四)——导出Minist模型,C++ OpenCV DNN进行识别

 

 

e470feefcf12d147a1aadec34c1feb27.jpeg

pyTorch入门(三)——GoogleNet和ResNet训练

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/135732.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

The Open Group亚太区总经理Chris Forde元旦贺词:踔厉奋发、笃行不怠,共赴新未来!

Happy New Year everyone, hope you are enjoying the holiday season, and perhaps planning your New Year’s resolutions. 大家新年快乐&#xff01;希望此刻您正在享受假期&#xff0c;或在规划自己的新年决心。 Now is the time for me, with you, to say goodbye to 202…

PDF怎么转换成Word?电脑必备的转换工具

电脑上的办公场景可以说是很多样了&#xff0c;而现在线上办公&#xff0c;线上会议&#xff0c;以及线上网课等的发展越来越全面&#xff0c;关于文件的编辑和传输也渐渐需要更多的软件来辅助我们办公。就像是PDF文件格式和Word文件格式这两种常见的格式&#xff0c;想要直接进…

小米路由器 R4A 刷原生 OpenWrt 后的风景

简 述: 继上篇 小米AX6S刷OpenWrt和开启OpenClash 后&#xff0c;手痒难耐&#xff0c;决定把小米路由器4A千兆版(R4A)路由器 给刷个原生的 OpenWrt。 文章目录背景刷成原生 OpenWrt原生 OpenWrt 基础操作开启 WiFiopkg 换源设置中文OpenClash 插件8M 之殇&#xff0c;终结Refe…

JavaSE学习(二)

1.基本数据类型转换 自动类型转换 1.java程序在进行赋值或运算的时候&#xff0c;会将精度小的类型自动转换为精度大的数据类型再进行计算 2.精度大的类型赋值给精度小的类型会报错&#xff0c;反之则会进行自动类型转换 int a4; floata1.1;这样写是错的&#xff0c;因为1.1是…

Uni-app + Vue3 + TS +Vite 创建项目

一、npx 与 npm 区别 npm 都很熟&#xff0c;可是与 npm 如此相似的 npx 是干嘛的呢&#xff1f;我们为甚要介绍 npx ? 由于 uni-app 官方提供创建命令使用的是 npx&#xff0c;所以我们先来了解下 npx 是干什么的&#xff1f;它与 npm 的区别。 npx 是 npm 的高级版本&…

java 出现unreachable statement异常 原因检查

unreachable statement异常&#xff1a; 今天在写代码的过程中&#xff0c;发现有行代码变红线&#xff0c;显示unreachable statement异常&#xff0c;但是代码本身没什么问题&#xff0c;通过查询资料发现其实就是该行代码不可执行的原因&#xff0c;出现该异常共有以下两种…

第三十八章 贪心算法——区间问题(上)

第三十八章 贪心策略——区间相关问题一、什么贪心策略&#xff1f;二、区间问题合集1、思路&#xff1a;2、问题1&#xff1a; 区间选点&#xff08;1&#xff09;问题&#xff08;2&#xff09;思路和证明a.思路b.证明&#xff08;3&#xff09;代码3、问题2&#xff1a;&…

Linux系统编程——基础篇

文章目录一、快捷键二、文件1.重要文件2.文件类型3.cp4.增加权限5.修改三、查找和检索四、安装五、压缩与解压六、vim的三种工作方式七、gcc编译四步骤八、静态库和动态库一、快捷键 Ctrla&#xff1a;光标移到开头 Ctrle&#xff1a;光标移到结尾 Ctrlu&#xff1a;清除整行 …

SQLSERVER 居然也能调 C# 代码 ?

一&#xff1a;背景 1. 讲故事 前些天看到一个奇怪的 Function 函数&#xff0c;调用的是 C# 链接库中的一个 UserLogin 方法&#xff0c;参考代码如下&#xff1a; CREATE FUNCTION dbo.clr_UserLogin (name AS NVARCHAR(100),password AS NVARCHAR(100) ) RETURNS INT AS…

Kali Linux中shutdown指令的用法3-1

在Kali Linux中&#xff0c;shutdown指令用于停止&#xff08;halt&#xff09;、关闭&#xff08;power off&#xff09;或者重启&#xff08;reboot&#xff09;系统。 1 语法格式 shutdown指令的语法如下所示 shutdown [OPTIONS] [TIME] [WALL] 其中&#xff0c;OPTIONS…

Qt、使用QToolButton和QStackedWidget的侧边栏(SideBar)的实现与实现原理解析

Qt、侧边栏&#xff08;SideBar&#xff09;的原理与实现&#xff08;附Demo&#xff09; 目录Qt、侧边栏&#xff08;SideBar&#xff09;的原理与实现&#xff08;附Demo&#xff09;1、简介2、侧边栏控件组成3、UI布局4、代码实现界面的切换Demo下载&#xff1a;https://git…

2023四川大学图书情报档案专业考研初试介绍(2023.1.02已更新)

文章目录川大图情基本情况2023年招生情况近5年录取数据复试2021-2022年复试线学硕复试线图情专硕复试线2021-2022年复试录取分数2022年学硕部分拟录取人员详细分数(不含调剂)专业课备考专业课资料博主所售资料一览667科目备考参考策略972科目备考方法参考目标分数川大图情基本情…

分享一套开源的springboot制造执行MES系统源码,带本地部署搭建教程+运行文档

全开源的一套超有价值的JAVA制造执行MES系统源码 亲测 带本地部署搭建教程 教你如何在本地运行运行起来。 开发环境&#xff1a;jdk1./1.8 tomcat mysql5.6springmvcmaven 需要源码学习&#xff0c;私信我获取。 一、系统概述&#xff1a; MES制造执行系统&#xff0c;其定位…

十分钟入门HBase特性与安装部署

1.写在前面 目前Hadoop生态的大数据组件都有一个其本身擅长的领域&#xff0c;并且目前看来&#xff0c;这个领域相对较窄&#xff0c;所以各位学生在大数据相关活动中&#xff0c;难免会有技术交集&#xff0c;最近学生在做离线数仓项目的时候&#xff0c;采用kylin技术组件&a…

【MySQL进阶教程】 存储引擎详细介绍

前言 本文为 【MySQL进阶教程】 存储引擎 相关知识介绍&#xff0c;下边具体将对MySQL体系结构&#xff0c;存储引擎介绍&#xff0c;存储引擎特点&#xff08;包含&#xff1a;InnoDB、MyISAM、Memory的特点及对比&#xff09;&#xff0c;存储引擎选择等进行详尽介绍~ &…

学习SpringCloudAlibaba(一)

一、为什么使用SpringCloud Alibaba 有了spring cloud这个微服务的框架&#xff0c;为什么又要使用spring cloud alibaba这个框架了&#xff1f; 最重要的原因在于spring cloud中的几乎所有的组件都使用Netflix公司的产品&#xff0c;然后在其基础上做了一层封装。然而Netfli…

走过 2022

“听过很多道理&#xff0c;依然过不好这一生”。每年写年终总结也是。但是审视自己在过去一年的表现依然是必需的。“吾日三省吾身”&#xff0c;更好的当然是每天都有所反思。世间很多事都离不开反馈&#xff0c;写总结就是一个很好的反馈。经历了过去荒诞的一年&#xff0c;…

开源虚拟机 qemu 安装以及使用方法 (helloos.img)

这篇文章里有 30Day Make OS 光盘的内容&#xff0c;感谢博主 https://blog.csdn.net/monster663/article/details/115919391 链接&#xff1a;https://pan.baidu.com/s/18dz8CuOxN21EAIU3os2KpA 提取码&#xff1a;qwer qemu 牛啤&#xff01; 从 https://www.qemu.org/down…

【阶段一】Python快速入门05篇:高级特性、pip工具、模块的使用、类(class)与异常处理

本篇的思维导图: 高级特性 列表生成式 现在有一个列表,你需要对该列表中的每个值求平方,然后将结果组成一个新列表。 描述 代码

分享101个PHP源码,总有一款适合您

链接&#xff1a;https://pan.baidu.com/s/1Jh2STRXhYU92KyGuaz_rsQ?pwdjvks 提取码&#xff1a;jvks PHP源码 分享101个PHP源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c;大家下载…