图像输入
前言
在上一篇文章中完成了语音输入,这一篇来写图像输入
正文
图像输入无非就是图片识别嘛,再通俗一点就是识别手机中的照片,分析里面的物品,然后进行垃圾分类。图像识别还是有很多的SDK可以使用的,这里面我目前用过的就是百度的图像识别,感觉还是蛮好的,而且有我之前的文章做普遍,那么本文是属于APP功能编写,这与单独写介绍SDK使用的文章完全是两回事。那么就来看看实践中怎么插入这个图像识别了。
如果你还有时间的话,不妨先去看看Android 百度图像识别(详细步骤+源码)
因为毕竟是写过一次的东西了,只是应用环境不同,所以下面就只是介绍业务逻辑和贴代码,不再去详细讲解。
一、创建平台应用
既然要用百度的SDK,自然要先去百度智能云注册登录,登录之后呢。点击管理控制台,然后点击左侧产品服务箭头左侧展开,找到图像识别点进去。
点击创建应用
输入相关的信息就可以了。
填写好资料后点击立即创建。
查看应用详情。
这里有三个关键的信息:AppID、API Key、Secret Key,这三个值在后面会用到,请使用自己创建应用时生成的值。现在先把它们放到常量里面,打开Constant,这里的四个常量,对应的值就是你在平台上申请应用产生的,记得使用自己的。
二、新建图像识别页面
在ui包下新建一个ImageInputActivity,对应的xml为activity_image_input.xml,创建好之后,再MainActivity页面中写一个按钮,点击之后进入刚才创建的这个图像识别页面。
修改activity_main.xml,在语音输入的下面加一个图像输入的按钮,代码如下:
<!--图像输入--> <com.google.android.material.button.MaterialButton style="@style/Widget.MaterialComponents.Button.UnelevatedButton" android:layout_width="match_parent" android:layout_height="@dimen/dp_60" android:layout_margin="@dimen/dp_16" android:gravity="center" android:insetTop="@dimen/dp_0" android:insetBottom="@dimen/dp_0" android:onClick="jumpImageInput" android:text="图像输入" android:textSize="@dimen/sp_16" android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar" app:backgroundTint="@color/colorPrimaryDark" app:cornerRadius="@dimen/dp_12" app:icon="@mipmap/icon_image_input" app:iconGravity="textStart" app:iconSize="@dimen/dp_24" />
进入到MainActivity中,新增一个方法jumpImageInput。
/** * 进入图像输入页面 */ public void jumpImageInput(View view) { gotoActivity(ImageInputActivity.class); }
下面来写ImageInputActivity页面的代码,写代码之前,先完成布局编写,修改activity_image_input,里面的代码如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:orientation="vertical"> <!--标题--> <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@color/white" android:elevation="@dimen/dp_2" app:navigationIcon="@mipmap/icon_back"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="图像输入" android:textColor="@color/black" android:textSize="18sp" /> </com.google.android.material.appbar.MaterialToolbar> <!--滑动控件--> <androidx.core.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent" android:overScrollMode="never"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center_horizontal" android:orientation="vertical"> <ImageView android:id="@+id/iv_picture" android:layout_width="@dimen/dp_200" android:layout_height="@dimen/dp_300" android:layout_marginTop="@dimen/dp_12" android:visibility="gone" /> <EditText android:id="@+id/et_image_url" android:layout_width="match_parent" android:layout_height="@dimen/dp_50" android:background="@drawable/shape_et_bg" android:hint="网络图片Url" android:layout_margin="@dimen/dp_1" android:textCursorDrawable="@drawable/cursor_style" android:paddingStart="@dimen/dp_12" android:paddingEnd="@dimen/dp_12" android:singleLine="true" android:imeOptions="actionGo" android:textSize="@dimen/sp_14" android:visibility="gone" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="@dimen/dp_12"> <com.google.android.material.button.MaterialButton android:id="@+id/btn_web_picture" style="@style/Widget.MaterialComponents.Button.UnelevatedButton" android:layout_width="0dp" android:layout_height="@dimen/dp_50" android:layout_marginEnd="@dimen/dp_6" android:layout_weight="1" android:gravity="center" android:insetTop="@dimen/dp_0" android:insetBottom="@dimen/dp_0" android:text="网络图片" android:textSize="@dimen/sp_16" android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar" app:backgroundTint="@color/colorPrimaryDark" app:cornerRadius="@dimen/dp_12" app:iconGravity="textStart" app:iconSize="@dimen/dp_24" /> <com.google.android.material.button.MaterialButton android:id="@+id/btn_open_album" style="@style/Widget.MaterialComponents.Button.UnelevatedButton" android:layout_width="0dp" android:layout_height="@dimen/dp_50" android:layout_marginStart="@dimen/dp_6" android:layout_marginEnd="@dimen/dp_6" android:layout_weight="1" android:gravity="center" android:insetTop="@dimen/dp_0" android:insetBottom="@dimen/dp_0" android:text="相册图片" android:textSize="@dimen/sp_16" android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar" app:backgroundTint="@color/colorPrimaryDark" app:cornerRadius="@dimen/dp_12" app:iconGravity="textStart" app:iconSize="@dimen/dp_24" /> <com.google.android.material.button.MaterialButton android:id="@+id/btn_take_photo" style="@style/Widget.MaterialComponents.Button.UnelevatedButton" android:layout_width="0dp" android:layout_height="@dimen/dp_50" android:layout_marginStart="@dimen/dp_6" android:layout_weight="1" android:gravity="center" android:insetTop="@dimen/dp_0" android:insetBottom="@dimen/dp_0" android:text="拍照图片" android:textSize="@dimen/sp_16" android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar" app:backgroundTint="@color/colorPrimaryDark" app:cornerRadius="@dimen/dp_12" app:iconGravity="textStart" app:iconSize="@dimen/dp_24" /> </LinearLayout> <!--图像识别结果--> <LinearLayout android:id="@+id/lay_recognition_result" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" android:orientation="vertical" android:visibility="gone"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_vertical"> <View android:layout_width="@dimen/dp_30" android:layout_height="@dimen/dp_1" android:background="@color/line_color" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="@dimen/dp_12" android:text="识别结果" /> <View android:layout_width="@dimen/dp_30" android:layout_height="@dimen/dp_1" android:background="@color/line_color" /> </LinearLayout> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rv_recognition_result" android:layout_width="match_parent" android:layout_height="wrap_content" android:overScrollMode="never" /> </LinearLayout> <!--垃圾分类结果--> <LinearLayout android:id="@+id/lay_classification_result" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" android:orientation="vertical" android:visibility="gone"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_vertical"> <View android:layout_width="@dimen/dp_30" android:layout_height="@dimen/dp_1" android:background="@color/line_color" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="@dimen/dp_12" android:text="分类结果" /> <View android:layout_width="@dimen/dp_30" android:layout_height="@dimen/dp_1" android:background="@color/line_color" /> </LinearLayout> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rv_classification_result" android:layout_width="match_parent" android:layout_height="wrap_content" android:overScrollMode="never" /> </LinearLayout> </LinearLayout> </androidx.core.widget.NestedScrollView> </LinearLayout>
然后回到ImageInputActivity页面,再写代码之前,先想一下这个页面要做什么?首先是获取百度的鉴权Token,然后进行图片识别,最后进行物品的垃圾分类。那么就需要三次网络请求,这里需要重写一个订阅。
三、网络订阅
这里需要增加一个网络访问地址,因为使用的是百度的API,而本身有一个天行的API地址,这里需要对两个地址进行一个控制。打开NetworkApi,在里面增加如下方法。
/** * 修改访问地址 * @param type */ private static void getBaseUrl(int type) { switch (type) { case 0://天行API地址 mBaseUrl = "http://api.tianapi.com"; break; case 1://百度SDK地址 mBaseUrl = "https://aip.baidubce.com"; break; default: break; } }
这个方法根据传入类型的不同,使用不同的网络地址,之前写博客时疏忽了,写的嗨了,漏掉了这一部分,现在补上。然后在createService方法中增加一个type参数,之后调用getBaseUrl方法获取访问地址。
现在你的这个createService方法改动了,那么其他调用了这个方法的地方也要做相应的改动,比如之前在做文字输入进行垃圾分类识别时,TextContract中的调用,之前是没有type的,现在你加一个0就可以了,0表示就是访问天行API。
其他的地方记也要修改,否则会报错的。改好之后,就可以来写这个图像识别到的订阅器了,如下:
在contract包下新建一个ImageContract类,里面的代码如下:
package com.llw.goodtrash.contract; import android.annotation.SuppressLint; import com.llw.goodtrash.api.ApiService; import com.llw.goodtrash.model.GetDiscernResultResponse; import com.llw.goodtrash.model.GetTokenResponse; import com.llw.goodtrash.model.TrashResponse; import com.llw.mvplibrary.base.BasePresenter; import com.llw.mvplibrary.base.BaseView; import com.llw.mvplibrary.network.NetworkApi; import com.llw.mvplibrary.network.observer.BaseObserver; import static com.llw.goodtrash.utils.Constant.*; /** * 图像输入页面访问网络 * * @author llw * @date 2021/3/30 15:28 */ public class ImageContract { public static class ImagePresenter extends BasePresenter<ImageView> { /** * 获取鉴权Token */ @SuppressLint("CheckResult") public void getToken() { ApiService service = NetworkApi.createService(ApiService.class, 1); service.getToken(GRANT_TYPE, API_KEY, API_SECRET) .compose(NetworkApi.applySchedulers(new BaseObserver<GetTokenResponse>() { @Override public void onSuccess(GetTokenResponse getTokenResponse) { if (getView() != null) { getView().getTokenResponse(getTokenResponse); } } @Override public void onFailure(Throwable e) { if (getView() != null) { getView().getTokenFailed(e); } } })); } /** * 获取图像识别结果 * * @param token 鉴权Token * @param image 图片base64 * @param url 网络图片url */ @SuppressLint("CheckResult") public void getDiscernResult(String token, String image, String url) { ApiService service = NetworkApi.createService(ApiService.class, 1); service.getDiscernResult(token, image, url) .compose(NetworkApi.applySchedulers(new BaseObserver<GetDiscernResultResponse>() { @Override public void onSuccess(GetDiscernResultResponse getTokenResponse) { if (getView() != null) { getView().getDiscernResultResponse(getTokenResponse); } } @Override public void onFailure(Throwable e) { if (getView() != null) { getView().getDiscernResultFailed(e); } } })); } /** * 搜索物品 * * @param word 物品名 */ @SuppressLint("CheckResult") public void searchGoods(String word) { ApiService service = NetworkApi.createService(ApiService.class, 0); service.searchGoods(word).compose(NetworkApi.applySchedulers(new BaseObserver<TrashResponse>() { @Override public void onSuccess(TrashResponse groupResponse) { if (getView() != null) { getView().getSearchResponse(groupResponse); } } @Override public void onFailure(Throwable e) { if (getView() != null) { getView().getSearchResponseFailed(e); } } })); } } public interface ImageView extends BaseView { /** * 获取鉴权Token * * @param response GetTokenResponse */ void getTokenResponse(GetTokenResponse response); /** * 获取鉴权Token异常返回 * * @param throwable 异常 */ void getTokenFailed(Throwable throwable); /** * 获取图像识别结果 * * @param response GetDiscernResultResponse */ void getDiscernResultResponse(GetDiscernResultResponse response); /** * 获取图像识别结果失败 * * @param throwable 异常 */ void getDiscernResultFailed(Throwable throwable); /** * 搜索物品返回 * * @param response TrashResponse */ void getSearchResponse(TrashResponse response); /** * 搜索物品异常返回 * * @param throwable 异常 */ void getSearchResponseFailed(Throwable throwable); } }
鉴权方法中的几个全局变量在Constant中定义,
/** * 鉴权Token */ public static final String TOKEN = "accessToken"; /** * 获取Token的时间 */ public static final String GET_TOKEN_TIME = "getTokenTime"; /** * Token有效期 */ public static final String TOKEN_VALID_PERIOD = "tokenValidPeriod"; /** * 百度鉴权认证参数值 */ public static final String GRANT_TYPE = "client_credentials"; /** * 百度图像识别 APP ID GoodTrash */ public static final String APP_ID = "23943795"; /** * 百度图像识别 APP Key GoodTrash */ public static final String API_KEY = "PAUCX7vSAd4ZBwv897GAfhEQ";
请注意,这里的值是我在百度开放平台上注册应用时生成的,请替换为自己的。
下面回到ImageInputActivity,修改代码后如下:
package com.llw.goodtrash.ui; import android.os.Bundle; import com.llw.goodtrash.R; import com.llw.goodtrash.contract.ImageContract; import com.llw.goodtrash.model.GetDiscernResultResponse; import com.llw.goodtrash.model.GetTokenResponse; import com.llw.goodtrash.model.TrashResponse; import com.llw.mvplibrary.mvp.MvpActivity; /** * 图像输入物品进行垃圾分类 * * @author llw * @date 2021/4/7 11:04 */ public class ImageInputActivity extends MvpActivity<ImageContract.ImagePresenter> implements ImageContract.ImageView { @Override public void initData(Bundle savedInstanceState) { } @Override public int getLayoutId() { return R.layout.activity_image_input; } @Override protected ImageContract.ImagePresenter createPresenter() { return new ImageContract.ImagePresenter(); } @Override public void getTokenResponse(GetTokenResponse response) { } @Override public void getTokenFailed(Throwable throwable) { } @Override public void getDiscernResultResponse(GetDiscernResultResponse response) { } @Override public void getDiscernResultFailed(Throwable throwable) { } @Override public void getSearchResponse(TrashResponse response) { } @Override public void getSearchResponseFailed(Throwable throwable) { } }
这里使用了MVP,通过P来处理M和V,三个网络请求对应六个返回,下面进行页面的初始化
四、编写页面代码
先声明一些变量
private static final String TAG = "ImageInputActivity"; /** * 打开相册 */ private static final int OPEN_ALBUM_CODE = 100; /** * 打开相机 */ private static final int TAKE_PHOTO_CODE = 101; /** * 鉴权Toeken */ private String accessToken; private Toolbar toolbar; private ImageView ivPicture; private EditText etImageUrl; private LinearLayout layRecognitionResult,layClassificationResult; private RecyclerView rvRecognitionResult,rvClassificationResult; private RxPermissions rxPermissions; private File outputImage;
然后新增一个initView的方法。
/** * 初始化 */ private void initView() { toolbar = findViewById(R.id.toolbar); ivPicture = findViewById(R.id.iv_picture); etImageUrl = findViewById(R.id.et_image_url); findViewById(R.id.btn_web_picture).setOnClickListener(this); findViewById(R.id.btn_open_album).setOnClickListener(this); findViewById(R.id.btn_take_photo).setOnClickListener(this); layRecognitionResult = findViewById(R.id.lay_recognition_result); layClassificationResult = findViewById(R.id.lay_classification_result); rvRecognitionResult = findViewById(R.id.rv_recognition_result); rvClassificationResult = findViewById(R.id.rv_classification_result); //设置页面状态栏 setStatubar(this, R.color.white, true); back(toolbar, true); rxPermissions = new RxPermissions(this); }
然后在initData中调用它。
@Override public void initData(Bundle savedInstanceState) { initView(); }
然后继承控件的点击监听回调
重写onClick方法。
@Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_web_picture://网络图片 break; case R.id.btn_open_album://相册图片 break; case R.id.btn_take_photo://拍照图片 break; default: break; } }
由于Token存在有效期的关系,因此不需要每一次都去获取,所以可以在第一次获取之后存入缓存,只要Token在有效期内都可以直接从缓存中获取,这样就可以少请求一次网络。
下面先写一个缓存的工具类。
在utils包下新增一个SPUtils类,里面的代码如下:
package com.llw.goodtrash.utils; import android.content.Context; import android.content.SharedPreferences; /** * SharedPreferences工具类 * * @author llw */ public class SPUtils { private static final String NAME = "config"; public static void putBoolean(String key, boolean value, Context context) { SharedPreferences sp = context.getSharedPreferences(NAME, Context.MODE_PRIVATE); sp.edit().putBoolean(key, value).commit(); } public static boolean getBoolean(String key, boolean defValue, Context context) { SharedPreferences sp = context.getSharedPreferences(NAME, Context.MODE_PRIVATE); return sp.getBoolean(key, defValue); } public static void putString(String key, String value, Context context) { SharedPreferences sp = context.getSharedPreferences(NAME, Context.MODE_PRIVATE); sp.edit().putString(key, value).commit(); } public static String getString(String key, String defValue, Context context) { if (context != null) { SharedPreferences sp = context.getSharedPreferences(NAME, Context.MODE_PRIVATE); return sp.getString(key, defValue); } return ""; } public static void putInt(String key, int value, Context context) { SharedPreferences sp = context.getSharedPreferences(NAME, Context.MODE_PRIVATE); sp.edit().putInt(key, value).commit(); } public static int getInt(String key, int defValue, Context context) { SharedPreferences sp = context.getSharedPreferences(NAME, Context.MODE_PRIVATE); return sp.getInt(key, defValue); } public static void putLong(String key, long value, Context context) { SharedPreferences sp = context.getSharedPreferences(NAME, Context.MODE_PRIVATE); sp.edit().putLong(key, value).commit(); } public static long getLong(String key, long defValue, Context context) { SharedPreferences sp = context.getSharedPreferences(NAME, Context.MODE_PRIVATE); return sp.getLong(key, defValue); } public static void remove(String key, Context context) { SharedPreferences sp = context.getSharedPreferences(NAME, Context.MODE_PRIVATE); sp.edit().remove(key).commit(); } }
然后在ImageInputActivity中写一个获取Token的方法,代码如下:
/** * 获取鉴权Token */ private String getAccessToken() { String token = SPUtils.getString(Constant.TOKEN, null, this); if (token == null) { //访问API获取接口 mPresenter.getToken(); } else { //则判断Token是否过期 if (isTokenExpired()) { //过期 mPresenter.getToken(); } else { accessToken = token; } } return accessToken; }
这里你的isTokenExpired()方法会报红,这是一个用来判断Token是否过期的方法。里面的代码如下:
/** * Token是否过期 * * @return */ private boolean isTokenExpired() { //获取Token的时间 long getTokenTime = SPUtils.getLong(Constant.GET_TOKEN_TIME, 0, this); //获取Token的有效时间 long effectiveTime = SPUtils.getLong(Constant.TOKEN_VALID_PERIOD, 0, this); //获取当前系统时间 long currentTime = System.currentTimeMillis() / 1000; return (currentTime - getTokenTime) >= effectiveTime; }
刚才在获取Token方法中,通过mPresenter.getToken();发起了一个网络请求,返回的结果有成功和失败,成功后会有Token返回,失败了会有失败原因返回。
修改如下方法:
/** * 获取鉴权Token成功返回 * @param response GetTokenResponse */ @Override public void getTokenResponse(GetTokenResponse response) { if (response != null) { //鉴权Token accessToken = response.getAccess_token(); //过期时间 秒 long expiresIn = response.getExpires_in(); //当前时间 秒 long currentTimeMillis = System.currentTimeMillis() / 1000; //放入缓存 SPUtils.putString(Constant.TOKEN, accessToken, this); SPUtils.putLong(Constant.GET_TOKEN_TIME, currentTimeMillis, this); SPUtils.putLong(Constant.TOKEN_VALID_PERIOD, expiresIn, this); } else { showMsg("Token为null"); } } /** * 获取Token失败返回 * @param throwable 异常 */ @Override public void getTokenFailed(Throwable throwable) { Log.d(TAG, "Token获取失败:"+throwable.toString()); }
我已经写了注释了,那么你就知道这个方法是做什么的了。
五、识别网络图片
我的想法是当我点击这个网络图片的按钮时,页面出现一个输入框,当我输入完成之后,点击键盘的回车直接识别,虽后隐藏这个输入框,嗯,就是这样。
首先来写点击网络图片时的业务逻辑代码。
case R.id.btn_web_picture://网络图片 etImageUrl.setVisibility(View.VISIBLE); etImageUrl.setOnKeyListener((view, keyCode, keyEvent) -> { if (keyCode == KeyEvent.KEYCODE_ENTER && keyEvent.getAction() == KeyEvent.ACTION_UP) { String webImageUrl = etImageUrl.getText().toString().trim(); String defaultWebImageUrl = "https://bce-baiyu.cdn.bcebos.com/14ce36d3d539b6004ef2e45fe050352ac65cb71e.jpeg"; String imageUrl = "".equals(webImageUrl) ?defaultWebImageUrl : webImageUrl; //识别网络图片Url showLoadingDialog(); Glide.with(context).load(imageUrl).into(ivPicture); mPresenter.getDiscernResult(accessToken,null,imageUrl); etImageUrl.setVisibility(View.GONE); } return false; }); break;
在这里发起了一个图片识别的请求,下面来看返回的方法处理。
/** * 图片识别成功返回 * @param response GetDiscernResultResponse */ @Override public void getDiscernResultResponse(GetDiscernResultResponse response) { if(response == null){ hideLoadingDialog(); showMsg("未获得相应的识别结果"); return; } ivPicture.setVisibility(View.VISIBLE); List<GetDiscernResultResponse.ResultBean> result = response.getResult(); if (result != null && result.size() > 0) { //显示识别结果 showDiscernResult(result); } else { hideLoadingDialog(); showMsg("未获得相应的识别结果"); } } /** * 图片识别成功失败 * @param throwable 异常 */ @Override public void getDiscernResultFailed(Throwable throwable) { Log.d(TAG, "图片识别失败:"+throwable.toString()); }
返回成功之后,如果数据不为空则显示要识别的图片,然后通过列表展示识别结果数据,
首先得有一个识别的结果列表item布局,item_distinguish_result_rv.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/item_distinguish_rv" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="1dp" android:background="#FFF" android:foreground="?attr/selectableItemBackground" android:padding="16dp"> <TextView android:id="@+id/tv_keyword" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#000" android:textSize="16sp" /> <TextView android:id="@+id/tv_root" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/tv_keyword" android:layout_marginTop="@dimen/dp_4" android:textSize="14sp" /> <TextView android:id="@+id/tv_score" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentEnd="true" /> </RelativeLayout>
下面写适配器代码,在adapter下新建一个DiscernResultAdapter类,代码如下:
package com.llw.goodtrash.adapter; import androidx.annotation.Nullable; import com.chad.library.adapter.base.BaseQuickAdapter; import com.chad.library.adapter.base.BaseViewHolder; import com.llw.goodtrash.R; import com.llw.goodtrash.model.GetDiscernResultResponse; import java.util.List; /** * 识别结果列表适配器 * @author llw */ public class DiscernResultAdapter extends BaseQuickAdapter<GetDiscernResultResponse.ResultBean, BaseViewHolder> { public DiscernResultAdapter(int layoutResId, @Nullable List<GetDiscernResultResponse.ResultBean> data) { super(layoutResId, data); } @Override protected void convert(BaseViewHolder helper, GetDiscernResultResponse.ResultBean item) { helper.setText(R.id.tv_keyword,item.getKeyword()) .setText(R.id.tv_root,item.getRoot()) .setText(R.id.tv_score,String.valueOf(item.getScore())) .addOnClickListener(R.id.item_distinguish_rv); } }
适配器和列表item都写好了,下面回到ImageInputActivity中新增方法showDiscernResult(),代码如下:
/** * 显示识别的结果列表 * * @param result */ private void showDiscernResult(List<GetDiscernResultResponse.ResultBean> result) { DiscernResultAdapter adapter = new DiscernResultAdapter(R.layout.item_distinguish_result_rv, result); rvRecognitionResult.setLayoutManager(new LinearLayoutManager(this)); rvRecognitionResult.setAdapter(adapter); //隐藏加载 hideLoadingDialog(); //显示弹窗 layRecognitionResult.setVisibility(View.VISIBLE); layClassificationResult.setVisibility(View.GONE); }
下面来运行一下:
不一定第一次就能识别出来,看你的人品。下面识别相册图片
六、识别相册图片
下面写点击这个相册图片按钮的业务逻辑,如下:
case R.id.btn_open_album://相册图片 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { rxPermissions.request( Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE) .subscribe(grant -> { if (grant) { //获得权限 openAlbum(); } else { showMsg("未获取到权限"); } }); } else { openAlbum(); } break;
打开相册的openAlbum()方法,代码如下:
/** * 打开相册 */ private void openAlbum() { Intent intent = new Intent(); intent.setAction(Intent.ACTION_PICK); intent.setType("image/*"); startActivityForResult(intent, OPEN_ALBUM_CODE); }
下面就是从相册返回的处理
@Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { showLoadingDialog(); if (requestCode == OPEN_ALBUM_CODE) { //打开相册返回 String[] filePathColumns = {MediaStore.Images.Media.DATA}; final Uri imageUri = Objects.requireNonNull(data).getData(); Cursor cursor = getContentResolver().query(imageUri, filePathColumns, null, null, null); cursor.moveToFirst(); int columnIndex = cursor.getColumnIndex(filePathColumns[0]); //获取图片路径 String imagePath = cursor.getString(columnIndex); cursor.close(); //识别 localImageDiscern(imagePath); } } else { showMsg("什么都没有"); } }
通过相册图片获取图片的路径,通过localImageDiscern方法将这个路径去转字节,再转base64。代码如下:
/** * 本地图片识别 */ private void localImageDiscern(String imagePath) { try { String token = getAccessToken(); //通过图片路径显示图片 Glide.with(this).load(imagePath).into(ivPicture); //按字节读取文件 byte[] imgData = FileUtil.readFileByBytes(imagePath); //字节转Base64 String imageBase64 = Base64Util.encode(imgData); //本地图片识别 mPresenter.getDiscernResult(token, imageBase64, null); } catch (IOException e) { e.printStackTrace(); } }
这里面还有两个工具类FileUtil和Base64Util。下面在utils包下新建一个FileUtil类,里面的代码如下:
package com.llw.goodtrash.utils; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; /** * 文件读取工具类 */ public class FileUtil { /** * 读取文件内容,作为字符串返回 */ public static String readFileAsString(String filePath) throws IOException { File file = new File(filePath); if (!file.exists()) { throw new FileNotFoundException(filePath); } if (file.length() > 1024 * 1024 * 1024) { throw new IOException("File is too large"); } StringBuilder sb = new StringBuilder((int) (file.length())); // 创建字节输入流 FileInputStream fis = new FileInputStream(filePath); // 创建一个长度为10240的Buffer byte[] bbuf = new byte[10240]; // 用于保存实际读取的字节数 int hasRead = 0; while ( (hasRead = fis.read(bbuf)) > 0 ) { sb.append(new String(bbuf, 0, hasRead)); } fis.close(); return sb.toString(); } /** * 根据文件路径读取byte[] 数组 */ public static byte[] readFileByBytes(String filePath) throws IOException { File file = new File(filePath); if (!file.exists()) { throw new FileNotFoundException(filePath); } else { ByteArrayOutputStream bos = new ByteArrayOutputStream((int) file.length()); BufferedInputStream in = null; try { in = new BufferedInputStream(new FileInputStream(file)); short bufSize = 1024; byte[] buffer = new byte[bufSize]; int len1; while (-1 != (len1 = in.read(buffer, 0, bufSize))) { bos.write(buffer, 0, len1); } byte[] var7 = bos.toByteArray(); return var7; } finally { try { if (in != null) { in.close(); } } catch (IOException var14) { var14.printStackTrace(); } bos.close(); } } } }
然后是Base64Util类,代码如下:
package com.llw.goodtrash.utils; /** * Base64 工具类 */ public class Base64Util { private static final char last2byte = (char) Integer.parseInt("00000011", 2); private static final char last4byte = (char) Integer.parseInt("00001111", 2); private static final char last6byte = (char) Integer.parseInt("00111111", 2); private static final char lead6byte = (char) Integer.parseInt("11111100", 2); private static final char lead4byte = (char) Integer.parseInt("11110000", 2); private static final char lead2byte = (char) Integer.parseInt("11000000", 2); private static final char[] encodeTable = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'}; public Base64Util() { } public static String encode(byte[] from) { StringBuilder to = new StringBuilder((int) ((double) from.length * 1.34D) + 3); int num = 0; char currentByte = 0; int i; for (i = 0; i < from.length; ++i) { for (num %= 8; num < 8; num += 6) { switch (num) { case 0: currentByte = (char) (from[i] & lead6byte); currentByte = (char) (currentByte >>> 2); case 1: case 3: case 5: default: break; case 2: currentByte = (char) (from[i] & last6byte); break; case 4: currentByte = (char) (from[i] & last4byte); currentByte = (char) (currentByte << 2); if (i + 1 < from.length) { currentByte = (char) (currentByte | (from[i + 1] & lead2byte) >>> 6); } break; case 6: currentByte = (char) (from[i] & last2byte); currentByte = (char) (currentByte << 4); if (i + 1 < from.length) { currentByte = (char) (currentByte | (from[i + 1] & lead4byte) >>> 4); } } to.append(encodeTable[currentByte]); } } if (to.length() % 4 != 0) { for (i = 4 - to.length() % 4; i > 0; --i) { to.append("="); } } return to.toString(); } }
下面可以直接运行了。
下面该识别拍照图片
七、识别拍照图片
点击拍照图片按钮的业务逻辑代码,
case R.id.btn_take_photo://拍照图片 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { rxPermissions.request( Manifest.permission.CAMERA) .subscribe(grant -> { if (grant) { //获得权限 turnOnCamera(); } else { showMsg("未获取到权限"); } }); } else { turnOnCamera(); } break;
turnOnCamera是用来打开相机的方法,里面的代码如下:
/** * 打开相机 */ private void turnOnCamera() { SimpleDateFormat timeStampFormat = new SimpleDateFormat("HH_mm_ss"); String filename = timeStampFormat.format(new Date()); //创建File对象 outputImage = new File(getExternalCacheDir(), "takePhoto" + filename + ".jpg"); Uri imageUri; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { imageUri = FileProvider.getUriForFile(this, "com.llw.goodtrash.fileprovider", outputImage); } else { imageUri = Uri.fromFile(outputImage); } //打开相机 Intent intent = new Intent(); intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); startActivityForResult(intent, TAKE_PHOTO_CODE); }
然后同样要在onActivityResult方法中添加相机返回的分支判断。
else if (requestCode == TAKE_PHOTO_CODE) { //拍照返回 String imagePath = outputImage.getAbsolutePath(); //识别 localImageDiscern(imagePath); }
添加位置如下:
下面就可以直接运行了。
OK,现在就来进行垃圾分类了。
八、垃圾分类
刚才通过图像识别已经拿到物品结果了,下面通过点击这个物品去进行垃圾分类。添加adapter的适配器,在showDiscernResult方法中添加如下代码
//添加列表Item点击 adapter.setOnItemChildClickListener((adapter1, view, position) -> { showLoadingDialog(); //垃圾分类 mPresenter.searchGoods(result.get(position).getKeyword()); });
添加位置如下:
下面进行搜索物品的返回数据处理。
/** * 搜索物品进行垃圾分类成功返回 * * @param response TrashResponse */ @Override public void getSearchResponse(TrashResponse response) { //请求成功 进行数据的渲染 if (response.getCode() == Constant.SUCCESS_CODE) { List<TrashResponse.NewslistBean> result = response.getNewslist(); if (result != null && result.size() > 0) { //显示分类结果 showClassificationResult(result); } else { showMsg("触及到了知识盲区"); } } else { hideLoadingDialog(); showMsg(response.getMsg()); } } @Override public void getSearchResponseFailed(Throwable throwable) { Log.d(TAG, "搜索物品进行垃圾分类失败:" + throwable.toString()); }
通过showClassificationResult方法进行物品垃圾分类结果显示,代码如下
/** * 显示物品分类结果 * @param result */ private void showClassificationResult(List<TrashResponse.NewslistBean> result) { SearchGoodsAdapter adapter = new SearchGoodsAdapter(R.layout.item_search_rv,result); rvClassificationResult.setLayoutManager(new LinearLayoutManager(context)); rvClassificationResult.setAdapter(adapter); //隐藏加载 hideLoadingDialog(); //显示弹窗 layClassificationResult.setVisibility(View.VISIBLE); }
下面运行一下:
下面再优化一下,就是让数据显示之后,滑动到屏幕底部,
修改activity_image_input.xml
给NestedScrollView控件添加一个id。
android:id="@+id/nestedScrollView"
然后进入ImageInputActivity中,初始化。
写一个方法
/** * 滑动到屏幕底部 */ private void scrollToEnd() { nestedScrollView.post(() -> { nestedScrollView.fullScroll(View.FOCUS_DOWN);//滚到底部 //nestedScrollView.fullScroll(ScrollView.FOCUS_UP);//滚到顶部 }); }
在显示列表数据时后调用,有两处。
第一处,图像识别的结果列表显示之后
第二处,显示物品垃圾分类结果显示之后
运行一下:
那么这个页面的功能就写完了。