前言
当前,深度学习有很多框架:tensorflow、pytorch、caffe、keras等。很多场景下,需要训练好的模型在移动端运行。移动端的框架又有很多TensorFlow Lite、Core ML、NCNN、MNN等等。
其中 tensorflow 所对应的移动端移植框架 TensorFlow Lite。在自己这个系列记录使用 调用tfile进行神经网络预测的android的实现。
整个记录为:
Android(1) —— Android studio 开发环境搭建
Android(2) —— Android Studio找不到连接的手机
Android(3) —— 环境配置、手机端界面设计
Android(4) —— 图像分类的*.tfile的使用 Classify.java
Android(5) —— 安卓机通过相机或相册获取图片PhotoUtil.java
Android(6) —— 主函数的详解 MainActivity.java
1 代码讲解
1.1 代码概述
该脚定义了在安卓端上实现神经网络(图片分类)的移植的主函数。是通过定义了
public class ClassifyLib
来实现的。会调用Classify.java
、PhotoUtil.java
两个脚本(这两个脚本前面的博客有详细的讲解)
先定义了类内的所需变量
- 三个按键:用于加载神经网络模型的按键,另外两个用于选择照片的来源:使用相机拍照的按键、从相册中选择照片的按键。
- 两个展示区域:获取到图片的展示区域,神经网络预测的结果展示区域。
- 实例化 Classify类,完成神经网络模型的加载、预测、结果解析的功能
public class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getName(); private static final int USE_PHOTO = 1001; private static final int START_CAMERA = 1002; private String camera_image_path; private ImageView show_image; // 手机界面的图像展示区域 private TextView result_text; // 手机界面的文字展示区域 private Button load_model; // 按键:加载模型 private Button use_photo; // 按键:使用相册图片 private Button start_photo; // 按键:使用相机拍照 private boolean load_result = false; // 是否加载模型成功 private Context context = null; private static final int REQUEST_EXTERNAL_STORAGE = 1; // 请求外部存储器 private static String[] PERMISSIONS_STORAGE = { "android.permission.READ_EXTERNAL_STORAGE", // 读取外部存储器 "android.permission.WRITE_EXTERNAL_STORAGE" }; // 写外部存储器 Classify test = new Classify(); // 新建一个对象,用于调用神经网络模型相关内容
1.2
onCreate
的重写1.2.1
onCreate
方法的简单介绍
- Activity中有一个名称叫 onCreate 的方法。该方法是在Activity创建时被系统调用,是一个Activity生命周期的开始。参数名称为savedInstanceState。
- Activity中另一个名称 onsaveInstanceState 方法。该方法是用来保存Activity的状态的。当一个Activity在生命周期结束前,会调用该方法保存状态。参数名称为savedInstanceState。
public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); } public void onSaveInstanceState(Bundle savedInstanceState){ super.onSaveInstanceState(savedInsanceState); }
在实际应用中,当一个Activity结束前,如果需要保存状态,就在onsaveInstanceState中, 将状态数据以key-value的形式放入到savedInstanceState中。这样,当一个Activity被创建时,就能从onCreate的参数savedInstanceState中获得状态数据。
比如:一个游戏在退出前,保存一下当前游戏运行的状态,当下次开启时能接着上次的继续玩下去。再比如:电子书程序,当一本小说被阅读到第199页后退出了(不管是内存不足还是用户自动关闭程序),当下次打开时,读者可能已忘记了上次已阅读到第几页了,但是,读者想接着上次的读下去。如果采用savedInstancsState参数,就很容易解决上述问题。
1.2.2
onCreate
的重写
- 由于会使用到读取相册,或者使用相机,所以要检测和申请相关权限。
- 然后要对三个按键进行初始化,进行监听,当触发时完成相应的功能
- 如果不是验证集(具有标签),就不用执行加载标签文件的操作
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); verifyStoragePermissions(this); // 验证存储权限 context = getApplicationContext(); init_view(); // 三个初始化 test.readCacheLabelFromLocalFile(context); // 加载标签文件,在 Classify.java脚本中实现 }
1.3 按键的初始化
init_view
在资源目录 res 文件夹下的所有资源(文件以及信息),系统都会自动在 gen 目录中的 R.java文件中生成一个对应且唯一的 id。我们可以通过这个唯一的 id去查找当前对应的相关内容。gen 目录下的所有文件是不需要我们手动添加和修改的,它是会自动在编译中生成,系统会自动对其进行操作。
- R.id.*** 其中的R指的是 gen 目录中的 R.java 文件;R.id.xxx 指的就是 R.java 中 xxx 的具体id。该控件 id 在我们编辑 xml 你就文件的时候给控件添加 Android::id 属性添加,R文件中会自动生成与之对应的id值。所以我们访问R.id.xxx 时,就会返回系统自动生成的 id 值。
- findViewById 安卓编程的定位函数,主要是引用.R文件里的引用名。
例如:Button button=(Button)findViewById(R.id.button01);
这样就引用了XML(res里的布局文件)文件里面的button,使得在写.java的按钮时能与XML里的一致。private void init_view() { request_permissions(); show_image = (ImageView) findViewById(R.id.show_image); result_text = (TextView) findViewById(R.id.result_text); result_text.setMovementMethod(ScrollingMovementMethod.getInstance()); load_model = (Button) findViewById(R.id.load_model); use_photo = (Button) findViewById(R.id.use_photo); start_photo = (Button) findViewById(R.id.start_camera);
监听事件有多种写法,这里使用的是 【匿名内部类实现事件监听】
- findViewById 代表着设置时间处理的监听器
- new View.OnClickListener() 代表时间监听器的接口
- onClick 代表着事件处理函数,也就是点击了 Button 按键后,就会进入到onClick函数里面,执行相应的功能
这里对三个按键分别设置了监听器
- 加载模型的点击事件:触发该按钮,就进行 *.tfile模型的读取
- 相册选择图片的点击事件:该功能的实现在
PhotoUtil.java
中实现- 拍照按钮的点击事件:该功能的实现在
PhotoUtil.java
中实现// 加载模型的点击事件 load_model.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { load_result = test.load_model(context);//加载模型,在 Classify.java脚本中实现 Toast.makeText(MainActivity.this, "模型加载已完成", Toast.LENGTH_SHORT).show(); } }); // 相册选择图片的点击事件 use_photo.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (!load_result) { Toast.makeText(MainActivity.this, "never load model", Toast.LENGTH_SHORT).show(); return; } PhotoUtil.use_photo(MainActivity.this, USE_PHOTO);//从相册获得一张图片 } }); // 拍照按钮的点击事件实现 start_photo.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (!load_result) { Toast.makeText(MainActivity.this, "never load model", Toast.LENGTH_SHORT).show(); return; } System.out.println("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"); camera_image_path = PhotoUtil.start_camera(MainActivity.this, START_CAMERA); >//从拍照获得一张图片的临时路径 System.out.println(camera_image_path); System.out.println("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"); } }); }
1.4 获取到图片后的操作
onActivityResult
1.4.1 基础知识的简单介绍
Activity的启动涉及到两个Activity——父Activity和子Activity,
- 父Activity是启动方,即调用startActivity()方法一方,
- 子Activity是被启动方,即被startActivity()方法打开的一方。
接下来分析,实现父Activity打开子Activity的方法 startActivityForResult(),和实现子Activity回传结果给父 Activity的方法 setResult()、onActivityResult()
【使用 intent extra 附加数据】
Inter 是四大组件之间和四大组间与操作系统之间通信的一种媒介工具。我们将需要传递的数据作为 entra 信息,附加给startActivity(Intent)
方法的 Inter 上发送出去。extra是一种 key-value 结构。
调用 Inter.putExtra() 方法将 extra 信息添加给 inter,Inter.putExtra() 方法有很多种形式。(1) 一个参数固定为 String 类型的 Key。(2) 一个参数值可以是多种数据类型:包括8中剧本数据类型 和 实现了 Serializable接口的类的实例对象。子 activity调用对应的 getXXextra()方法对extra进行解析即可获取父 activity发过来的数据。【父 acitvity 调用 startActivityForResult() 方法,启动子activity】
父 activity为了从子 activity中获取返回结果,所以调用启动子 activity的另外一种方法startActivityForResult(Intent intent, intrequestCode)
,该方法接收两个参数:(1) 一个是携带了 extra的 Intent (2) 另一个参数是请求代码 requestCode。
请求代码是发送给子 activity,然后再返回给父 activity的用户定义整数值。(当一个activity启动多个不同类型的字 activity,且需要判断区分消息回馈放时,会要使用到请求码进行区分)【子 acitvity 调用 setResult() 方法,回传数据给父 activity】
实现子 activity返回值给父 activity,有以下两种 setResult() 方法可供调用:
public final void setResult(int resultCode)
public final void setResult(int resultCode)
, 通常来说,参数 result-code 可以是以下两个系统预定义常量中的任何一个:【ACTIVITY.RESULT_OK】、【 ACTIVITY_RESULT_CANCELED】。 使用 setResult() 方法代码附在后面。
如果子 activity 是 startActivityForResult() 方法启动的,结果代码则总是会返回给父 activity。子 activity可以不调用 setResult() 方法,如果不需要区分附加在 intent上的结果或者其他信息,可以让操作系统发送默认的结果代码。Intent data = new Intent(); data.putExtra(EXTRA_ANSWER_SHOWN, isAnswerShown); setResult(RESULT_OK, data);
- 【父 activity 调用 onActivityResult() 方法接收子 activity 返回的数据】
在用户单击后退,从子 activity回到父 activity时,ActivityManager会调用父 activity的onActivityResult(int requestCode,int resultCode, Intent data)
方法。该方法接收三个参数:
- requestCode :请求的标识,用于分辨父activity获取到的数据来源于哪个子 activity
- resultCode :子activity返回的标识,代表着返回数据是否成功
- Intent :setResult() 返回的结果
1.4.2
onActivityResult
的具体定义在本工程中这个命令执行的时间:
- (1) 拍完照片,返回到了软件打开时的界面后执行;
- (2) 从相册中选择了要用于神经网络预测的照片后,返回到了软件打开时的界面之后执行。
执行的内容:
- 获取到的图像的路径,并展示图片
- 读取图片、CNN预测、返回预测结果并展示(在
Classify.java
脚本中实现)@Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode,resultCode,data); String image_path; RequestOptions options = new >RequestOptions().skipMemoryCache(true).diskCacheStrategy(DiskCacheStrategy.NONE); if (resultCode == Activity.RESULT_OK) { switch (requestCode) { // 当选择使用相册中的照片 case USE_PHOTO: if (data == null) { Log.w(TAG, "user photo data is null"); return; } // 获取到图片的 Uri Uri image_uri = data.getData(); // 在手机图像展示区域,展示图像 Glide.with(MainActivity.this).load(image_uri).apply(options).into(show_image); // get image path from uri image_path = PhotoUtil.get_path_from_URI(MainActivity.this, image_uri); System.out.println("----------------------------------------------------------------"); System.out.println(image_path); System.out.println("----------------------------------------------------------------"); // predict image //predict_image(image_path); BitmapFactory.Options opt = new BitmapFactory.Options(); opt.inJustDecodeBounds = false; Bitmap bmp = BitmapFactory.decodeFile(image_path, opt); String tmp1 = test.predict_image(bmp);//执行推理与预测 result_text.setText(tmp1); break; case START_CAMERA: // show photo Glide.with(MainActivity.this).load(camera_image_path).apply(options).into(show_image); // predict image BitmapFactory.Options opt1 = new BitmapFactory.Options(); opt1.inJustDecodeBounds = false; Bitmap bmp1 = BitmapFactory.decodeFile(camera_image_path, opt1); String tmp2 = test.predict_image(bmp1);//执行推理与预测 result_text.setText(tmp2); break; } } }
1.5 检测和申请相关权限
Android 权限相关内容,稍微有点长,所以在这个链接中进行介绍。下面直接展示在该工程中的代码编写。
1.5.1 验证存储权限
public static void verifyStoragePermissions(Activity activity) { try { //检测是否有写的权限 int permission = ActivityCompat.checkSelfPermission(activity, "android.permission.WRITE_EXTERNAL_STORAGE"); if (permission != PackageManager.PERMISSION_GRANTED) { // 没有写的权限,去申请写的权限,会弹出对话框 ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,REQUEST_EXTERNAL_STORAGE); } } catch (Exception e) { e.printStackTrace(); } }
1.5.2 检测和申请权限
// request permissions private void request_permissions() { List<String> permissionList = new ArrayList<>(); if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { permissionList.add(Manifest.permission.CAMERA); } if (ContextCompat.checkSelfPermission(this, >Manifest.permission.WRITE_EXTERNAL_STORAGE) != >PackageManager.PERMISSION_GRANTED) { permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); } if (ContextCompat.checkSelfPermission(this, >Manifest.permission.READ_EXTERNAL_STORAGE) != >PackageManager.PERMISSION_GRANTED) { permissionList.add(Manifest.permission.READ_EXTERNAL_STORAGE); } // if list is not empty will request permissions if (!permissionList.isEmpty()) { ActivityCompat.requestPermissions(this, permissionList.toArray(new String[permissionList.size()]), 1); } }
1.5.3 权限的回调函数
@Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, >@NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch (requestCode) { case 1: if (grantResults.length > 0) { for (int i = 0; i < grantResults.length; i++) { int grantResult = grantResults[i]; if (grantResult == PackageManager.PERMISSION_DENIED) { String s = permissions[i]; Toast.makeText(this, s + " permission was denied", Toast.LENGTH_SHORT).show(); } } } break; } }