您现在的位置是:首页 >技术杂谈 >android-ncnn-yolov5项目升级网站首页技术杂谈

android-ncnn-yolov5项目升级

zsprb1 2023-07-08 12:00:02
简介android-ncnn-yolov5项目升级

原项目地址GitHub - nihui/ncnn-android-yolov5: The YOLOv5 object detection android example

倪辉大佬的这个yolov5安卓手机部署项目,提供了相册选图,CPU/GPU检测。

我需要添加一个拍照,去掉CPU按钮,拍照或者选图之后进行剪裁,以及将检测结果图保存到本地。

遂零基础自学了一年的Android开发。总算是小成。

参考我之前的Android开发 拍照+读取相册+保存到本地(单页面版)_zsprb1的博客-CSDN博客

项目文件结构

 

MainActivity.java被我整合到分支页面里,所以改成了CV.java

package com.neo.jiaruassistant;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;

import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;


public class CV extends AppCompatActivity {
    public static final int TAKE_PHOTO = 1;
    public static final int CHOOSE_PHOTO = 2;
    private static final int REQUESTCODE_CORP = 3;
    private ImageView picture;
    private Button GPUDection = null;
    private Button pictureSave = null;
    private Uri imageUri, uritempFile;
    private Bitmap bitmap = null;
    private Bitmap yourSelectedImage = null;

    private YoloV5Ncnn yolov5ncnn = new YoloV5Ncnn();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_cv);

        /**
         *初始化yolov5ncnn
         */
        boolean ret_init = yolov5ncnn.Init(getAssets());
        if (!ret_init) {
            Log.e("CV", "yolov5ncnn Init failed");
        }
        GPUDection = super.findViewById(R.id.GPUDetection);
        pictureSave = super.findViewById(R.id.pictureSave);
        picture = findViewById(R.id.picture);

        GPUDection.setOnClickListener(new GPUDectionFuntion());
        pictureSave.setOnClickListener(new pictureSaveFunction());

        Button chooseFromAlbum = findViewById(R.id.choose_from_album);
        chooseFromAlbum.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (ContextCompat.checkSelfPermission(CV.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
                    ActivityCompat.requestPermissions(CV.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, CHOOSE_PHOTO);
                } else {
                    ///跳转相册 Intent.ACTION_PICK 、Intent.ACTION_GET_CONTENT 、Intent.ACTION_OPEN_DOCUMENT 三者任选其一
                    Intent intent = new Intent(Intent.ACTION_PICK);

                    intent.setType("image/*");
                    startActivityForResult(intent, CHOOSE_PHOTO);//打开相册
                }
            }
        });
        Button takePhoto = findViewById(R.id.take_photo);
        takePhoto.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 动态申请权限
                // 创建一个File对象,用于保存摄像头拍下的图片,这里把图片命名为output_image.jpg
                // 并将它存放在手机SD卡的应用关联缓存目录下
                File outputImage = new File(getExternalCacheDir(), "output_image.jpg");
                // 对照片的更换设置
                try {
                    // 如果上一次的照片存在,就删除
                    if (outputImage.exists()) {
                        outputImage.delete();
                    }
                    // 创建一个新的文件
                    outputImage.createNewFile();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                // 如果Android版本大于等于7.0
                if (Build.VERSION.SDK_INT >= 24) {
                    // 将File对象转换成一个封装过的Uri对象
                    imageUri = FileProvider.getUriForFile(CV.this, "com.neo.jiaruassitant.cameraalbumtest.fileprovider", outputImage);
                    Log.d("CV", outputImage.toString() + "手机系统版本高于Android7.0");
                } else {
                    // 将File对象转换为Uri对象,这个Uri标识着output_image.jpg这张图片的本地真实路径,Uri.fromFile是个过时的代码
                    Log.d("CV", outputImage.toString() + "手机系统版本低于Android7.0");
                    imageUri = Uri.fromFile(outputImage);
                }
                //相机拍照后进行裁剪
                // 动态申请权限
                if (ContextCompat.checkSelfPermission(CV.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                    ActivityCompat.requestPermissions(CV.this, new String[]{Manifest.permission.CAMERA}, TAKE_PHOTO);
                } else {
                    // 启动相机程序
                    Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
                    // 指定图片的输出地址为imageUri
                    intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
                    startActivityForResult(intent, TAKE_PHOTO);
                }
            }
        });
    }

    private class GPUDectionFuntion implements View.OnClickListener {
        public void onClick(View view) {
            Toast.makeText(getApplicationContext(), "图片编辑功能开发中...敬请期待", Toast.LENGTH_SHORT).show();
            /**
             * 代码解释:没有选图就识别,啥也不会做。如果选好了图,将这个bitmap图丢进自定义的yolov5ncnn中进行推理,获得到objects,然后调用showObjects进行展示目标检测结果。
             */
            if (yourSelectedImage == null)
                return;

            YoloV5Ncnn.Obj[] objects = yolov5ncnn.Detect(yourSelectedImage, true);

            showObjects(objects);
        }
    }

    /**
     * 显示目标框以及类名和置信度
     */
    private void showObjects(YoloV5Ncnn.Obj[] objects) {
        if (objects == null) {
            picture.setImageBitmap(bitmap);
            return;
        }

        // draw objects on bitmap
        Bitmap rgba = bitmap.copy(Bitmap.Config.ARGB_8888, true);

        final int[] colors = new int[]{
                Color.rgb(54, 67, 244),
                Color.rgb(99, 30, 233),
                Color.rgb(176, 39, 156),
                Color.rgb(183, 58, 103),
                Color.rgb(181, 81, 63),
                Color.rgb(243, 150, 33),
                Color.rgb(244, 169, 3),
                Color.rgb(212, 188, 0),
                Color.rgb(136, 150, 0),
                Color.rgb(80, 175, 76),
                Color.rgb(74, 195, 139),
                Color.rgb(57, 220, 205),
                Color.rgb(59, 235, 255),
                Color.rgb(7, 193, 255),
                Color.rgb(0, 152, 255),
                Color.rgb(34, 87, 255),
                Color.rgb(72, 85, 121),
                Color.rgb(158, 158, 158),
                Color.rgb(139, 125, 96)
        };

        Canvas canvas = new Canvas(rgba);

        Paint paint = new Paint();
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(4);

        Paint textbgpaint = new Paint();
        textbgpaint.setColor(Color.WHITE);
        textbgpaint.setStyle(Paint.Style.FILL);

        Paint textpaint = new Paint();
        textpaint.setColor(Color.BLACK);
        textpaint.setTextSize(26);
        textpaint.setTextAlign(Paint.Align.LEFT);

        for (int i = 0; i < objects.length; i++) {
            paint.setColor(colors[i % 19]);

            canvas.drawRect(objects[i].x, objects[i].y, objects[i].x + objects[i].w, objects[i].y + objects[i].h, paint);

            // draw filled text inside image
            {
                String text = objects[i].label + " = " + String.format("%.1f", objects[i].prob * 100) + "%";

                float text_width = textpaint.measureText(text);
                float text_height = -textpaint.ascent() + textpaint.descent();

                float x = objects[i].x;
                float y = objects[i].y - text_height;
                if (y < 0)
                    y = 0;
                if (x + text_width > rgba.getWidth())
                    x = rgba.getWidth() - text_width;

                canvas.drawRect(x, y, x + text_width, y + text_height, textbgpaint);

                canvas.drawText(text, x, y - textpaint.ascent(), textpaint);
            }
        }

        picture.setImageBitmap(rgba);
    }

    private class pictureSaveFunction implements View.OnClickListener {
        public void onClick(View view) {

            BitmapDrawable bmpDrawable = (BitmapDrawable) picture.getDrawable();
            if (bmpDrawable != null) {
                Bitmap bitmap = bmpDrawable.getBitmap();
                saveToSystemGallery(bitmap);//将图片保存到本地
                Toast.makeText(getApplicationContext(), "图片已保存到相册!", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(getApplicationContext(), "请先拍照或者选图,识别之后,再保存", Toast.LENGTH_SHORT).show();
            }
        }
    }

    public void saveToSystemGallery(Bitmap bmp) {
        // 首先保存图片
        File appDir = new File(Environment.getExternalStorageDirectory(), "MyAlbums");
        if (!appDir.exists()) {
            appDir.mkdir();
        }
        String fileName = System.currentTimeMillis() + ".jpg";
        File file = new File(appDir, fileName);
        try {
            FileOutputStream fos = new FileOutputStream(file);
            bmp.compress(Bitmap.CompressFormat.JPEG, 100, fos);
            fos.flush();
            fos.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return;
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }

        Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
        Uri uri = Uri.fromFile(file);
        intent.setData(uri);
        sendBroadcast(intent); // 发送广播,通知图库更新
    }

    // 使用startActivityForResult()方法开启Intent的回调
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {

        switch (requestCode) {
            case TAKE_PHOTO:
                if (resultCode == RESULT_OK) {
                    cropPhoto(imageUri);//拍照之后,调用剪裁
                    try {
                        bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));// 将图片解析成Bitmap对象
                        yourSelectedImage = bitmap.copy(Bitmap.Config.ARGB_8888, true);
                        picture.setImageBitmap(bitmap);
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                        return;
                    }
                }
                super.onActivityResult(requestCode, resultCode, data);
                break;
            case CHOOSE_PHOTO:
                if (resultCode == RESULT_OK && null != data) {
                    Uri selectedImage = data.getData();//从相册选取照片后进行裁剪
                    cropPhoto(selectedImage);//选图之后,调用剪裁
                    try {
                        bitmap = decodeUri(selectedImage);//将选择的图片解析成Bitmap对象
                        yourSelectedImage = bitmap.copy(Bitmap.Config.ARGB_8888, true);
                        picture.setImageBitmap(bitmap);
                    } catch (FileNotFoundException e) {
                        Log.e("CV", "FileNotFoundException");
                        return;
                    }
                }
                super.onActivityResult(requestCode, resultCode, data);
                break;
            case REQUESTCODE_CORP: //剪切被调用之后,图片返回,并将bitmap传给yourSelectedImage
                if (resultCode == RESULT_OK && null != data) {
                    try {
                        bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uritempFile));//这里是局部变量bitmap
                        yourSelectedImage = bitmap.copy(Bitmap.Config.ARGB_8888, true);
                        picture.setImageBitmap(bitmap);//将剪裁后的图片显示到控件imageview中
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                        return;
                    }
                    System.out.println("图片裁剪完毕");
                }
                super.onActivityResult(requestCode, resultCode, data);
                break;
            default:
                break;
        }
    }

    private Bitmap decodeUri(Uri selectedImage) throws FileNotFoundException {
        // Decode image size
        BitmapFactory.Options o = new BitmapFactory.Options();
        o.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(getContentResolver().openInputStream(selectedImage), null, o);

        // The new size we want to scale to
        final int REQUIRED_SIZE = 640;

        // Find the correct scale value. It should be the power of 2.
        int width_tmp = o.outWidth, height_tmp = o.outHeight;
        int scale = 1;
        while (true) {
            if (width_tmp / 2 < REQUIRED_SIZE
                    || height_tmp / 2 < REQUIRED_SIZE) {
                break;
            }
            width_tmp /= 2;
            height_tmp /= 2;
            scale *= 2;
        }

        // Decode with inSampleSize
        BitmapFactory.Options o2 = new BitmapFactory.Options();
        o2.inSampleSize = scale;
        Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(selectedImage), null, o2);

        // Rotate according to EXIF
        int rotate = 0;
        try {
            ExifInterface exif = new ExifInterface(getContentResolver().openInputStream(selectedImage));
            int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
            switch (orientation) {
                case ExifInterface.ORIENTATION_ROTATE_270:
                    rotate = 270;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_180:
                    rotate = 180;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_90:
                    rotate = 90;
                    break;
            }
        } catch (IOException e) {
            Log.e("CV", "ExifInterface IOException");
        }

        Matrix matrix = new Matrix();
        matrix.postRotate(rotate);
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
    }

    /**
     * 自带的裁剪
     *
     * @param uri
     */
    private void cropPhoto(Uri uri) {
        Intent intent = new Intent("com.android.camera.action.CROP");
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        intent.setDataAndType(uri, "image/*");
        // 下面这个crop=true是设置在开启的Intent中设置显示的VIEW可裁剪
        intent.putExtra("crop", "true");
        // XY设置为1:1的时候,是圆形
        intent.putExtra("aspectX", 1);
        intent.putExtra("aspectY", 1);
        // outputX outputY 是裁剪图片宽高
        intent.putExtra("outputX", 360);//最大360,再大就闪退。
        intent.putExtra("outputY", 360);//最大360,再大就闪退。
        intent.putExtra("return-data", true);
        Log.e("tag", "intent====" + intent);
//不能以这种形式返回图片数据 因为现在图片很大 我们得以 uri 方式返回
        //创建 uri getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) 这个地方会出现 注意点二 或 注意点三 的报错
        uritempFile = Uri.parse("file://" + "/" + getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) + "/" + "small.jpg");
        System.out.println("uritempFile => " + uritempFile);
        //设置 uri
        intent.putExtra(MediaStore.EXTRA_OUTPUT, uritempFile);
        //设置格式
        startActivityForResult(intent, REQUESTCODE_CORP);
    }

}

布局页面activity_cv.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".CV">


    <TextView
        android:id="@+id/textView4"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/ring"
        android:gravity="center"
        android:text="@string/CV_thyroid" />
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:gravity="center">
        <Button
            android:id="@+id/take_photo"
            android:layout_width="72dp"
            android:layout_height="72dp"
            android:background="@drawable/ic_4"
            android:text="" />

        <Button
            android:id="@+id/choose_from_album"
            android:layout_width="72dp"
            android:layout_height="72dp"
            android:background="@drawable/ic_3"
            android:text="" />

        <Button
            android:id="@+id/GPUDetection"
            android:layout_width="72dp"
            android:layout_height="72dp"
            android:background="@drawable/ic_2"
            android:text="" />

        <Button
            android:id="@+id/pictureSave"
            android:layout_width="72dp"
            android:layout_height="72dp"
            android:background="@drawable/ic_5" />
    </LinearLayout>

    <ImageView
        android:id="@+id/picture"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/border"
        android:gravity="center_horizontal"
        android:scaleType="fitCenter" />
    <!--
    matrix默认,从ImageView的左上角开始绘制原图,原图超过ImageView的部分作裁剪处理。
    center保持原图的大小,显示在ImageView的中心。当原图的size大于ImageView的size,超过部分裁剪处理。
    centerCrop宽和高都要填满,原图超过ImageView的部分作裁剪处理
    centerInside原图完全显示为目的,小于ImageView的size,则原图的size不作任何处理
    fitCenter把原图按比例扩大或缩小到ImageView的ImageView的高度
    fitXY把原图按照指定的大小在View中显示,拉伸显示图片,不保持原比例,填满
    -->

</LinearLayout>

原包名com.tencent.yolov5ncnn换成了我自己的com.neo.jiaruassistant。YoloV5Ncnn.java里面的包名替换掉。对应yolov5ncnn_jni.cpp文件里面的路径都要改,不然会初始化失败。

JNIEXPORT jboolean JNICALL Java_com_neo_jiaruassistant_YoloV5Ncnn_Init(JNIEnv* env, jobject thiz, jobject assetManager)

jclass localObjCls = env->FindClass("com/neo/jiaruassistant/YoloV5Ncnn$Obj");

constructortorId = env->GetMethodID(objCls, "<init>", "(Lcom/neo/jiaruassistant/YoloV5Ncnn;)V");

JNIEXPORT jobjectArray JNICALL Java_com_neo_jiaruassistant_YoloV5Ncnn_Detect(JNIEnv* env, jobject thiz, jobject bitmap, jboolean use_gpu)

 这里一直有个红色报错

The type specifier does not match method '<init>()'. 

但是,好像不影响程序正常运行,原因不清楚,如果有人知道,请告诉我哦!

最后,如果想检测结果用中文

static const char* class_names[] = {
    "物品1","物品2","物品3"
};
括号里的所有类,都改成中文即可。

CMakeList.txt文件

project(yolov5ncnn)

cmake_minimum_required(VERSION 3.4.1)

set(ncnn_DIR ${CMAKE_SOURCE_DIR}/${ANDROID_ABI}/lib/cmake/ncnn)
find_package(ncnn REQUIRED)

add_library(yolov5ncnn SHARED yolov5ncnn_jni.cpp)

target_link_libraries(yolov5ncnn
    ncnn

    jnigraphics
)

build.gradle文件

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

android {
    namespace 'com.neo.jiaruassistant'
    compileSdk 33

    defaultConfig {
        applicationId "com.neo.jiaruassistant"
        archivesBaseName = "$applicationId"
        minSdk 28
        targetSdk 33
        versionCode 2
        versionName "${releaseTime()}"//每次打包时,程序按当前时间进行版本号设置
        ndk {
            moduleName "ncnn"
            abiFilters "armeabi-v7a", "arm64-v8a"
        }
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    externalNativeBuild {
        cmake {
            version "3.10.2"
            path file('src/main/jni/CMakeLists.txt')
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            android.applicationVariants.all { variant ->
                variant.outputs.all {
                    outputFileName = "my_${releaseTime()}.apk"
                }
            }
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        viewBinding true
    }
}


dependencies {

    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.5.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
    implementation 'net.sourceforge.jexcelapi:jxl:2.6.12'
    implementation 'junit:junit:4.12'
    implementation 'androidx.core:core-ktx:1.3.1'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

def releaseTime() {
    return new Date().format("yyyyMMddHHmmss", TimeZone.getTimeZone("UTC"))
}

感谢强大的ChatGPT在我无人可问的时候,横空出世!

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。