您现在的位置是:首页 >技术杂谈 >android-ncnn-yolov5项目升级网站首页技术杂谈
android-ncnn-yolov5项目升级
简介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在我无人可问的时候,横空出世!
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。