基本步骤:打开相机和相册获取一张照片,转为base64,异步上传到百度ai平台,拿到返回结果。
总结知识点:
- 安卓系统相机和相册的使用
- 百度开放平台识别接口的使用
- okhttp
- 使用handler异步上传图片
- 使用bundle线程间传递数据
- 识别结果JSON的解析
- dialog的使用
由于几个比较简单就不详述,重点讲下相机,百度,handler,bundle
通过相机和相册获取照片
权限注册:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
- 弹出一个dialog询问是相机还是相册:
String items [] = {"相机","相册"};// 用于作为dialog的选项
AlertDialog dialog = new AlertDialog.Builder(getContext())
.setTitle("请选择图片获取途径")
.setItems(items, new DialogInterface.OnClickListener() {
@Override // dialog的点击事件
public void onClick(DialogInterface dialogInterface, int i) {
switch (i){
case 0:
openCamera();
break;
case 1:
openAlbum();
break;
}
}
}).create();
dialog.show();
2.详解openCamera()
public static final int PHOTO_REQUEST_CAMERA = 1;// 拍照
private Intent intent_camera;
public static File tempFile;
private void openCamera() {
//獲取系統版本
int currentapiVersion = android.os.Build.VERSION.SDK_INT;
// 创建隐式调用相机的intent
intent_camera = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// 判断存储卡是否可以用,可用进行存储
if (hasSdcard()) {
// 取得当前时间命名文件
SimpleDateFormat timeStampFormat = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss");
String filename = timeStampFormat.format(new Date());
// 创建存放照片的文件
tempFile = new File(Environment.getExternalStorageDirectory(), filename + ".jpg");
if (currentapiVersion < 24) {
// 从文件中创建uri
imageUri = Uri.fromFile(tempFile);
//将当前uri放到系统指定的位置,(系统会自动将照片放到这个位置的uri中)
intent_camera.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent_camera, PHOTO_REQUEST_CAMERA);
} else {
//兼容android7.0 使用共享文件的形式
contentValues = new ContentValues(1);
contentValues.put(MediaStore.Images.Media.DATA, tempFile.getAbsolutePath());
imageUri = getActivity().getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
//检查是否有存储权限,以免崩溃
if ( ContextCompat.checkSelfPermission(getActivity(),
Manifest.permission.WRITE_EXTERNAL_STORAGE )
!= PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(getActivity(),
Manifest.permission.CAMERA)
!=PackageManager.PERMISSION_GRANTED ) {
//申请WRITE_EXTERNAL_STORAGE权限
ActivityCompat.requestPermissions(getActivity(),
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.CAMERA},
REQUEST_PERMISSSION_CAMERA);
}else { // 有权限
intent_camera.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
// 开启一个带有返回值的Activity,请求码为PHOTO_REQUEST_CMERA
startActivityForResult(intent, PHOTO_REQUEST_CAMERA);
}
}
}
}
首先创建一个用于启动系统相机的intent和一个用于存放照片的file,接着获取当前的手机系统版本,(由于安卓7.0前后uri的获取方式有了变化,7.0以后使用共享文件的形式获取uri),如果有物理内存,获取当前的时间用于命名相册file,相对路径构造file,如果是7.0以下版本,直接用Uri.fromFile(tempFile)获得uri,放到系统指定的位置(MediaStore.EXTRA_OUTPUT),系统会将照片复制到这个uri上(我也不知道是不是复制啊,反正就是在拍照前指定MediaStore.EXTRA_OUTPUT一个uri,拍照后这个uri就是照片),然后startActivityForResult(intent_camera, PHOTO_REQUEST_CAMERA);在回调中拿到uri(照片),7.0以上除了uri获取不一样其他一模一样,就是那三行,核心使用contentValues,思路和7.0以下一样。
// 判断是否含有存储空间
private boolean hasSdcard() {
return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
}
3.详解openAlbum()
public static final int PHOTO_REQUEST_ALBUM = 0; // 相册
private Intent intent_album;
// 打开相册
private void openAlbum() {
intent_album = new Intent(Intent.ACTION_PICK);
intent_album.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,"image/*");
startActivityForResult(intent_album,PHOTO_REQUEST_ALBUM);
}
先创建一个隐式打开相册的intent,(其实这个是我百度过来的,没看懂,缺个解析,19.4.6)
4.处理回调
// 处理活动回调
Bitmap bitmap;
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode){
case PHOTO_REQUEST_CAREMA : // 处理相机的回调
// 自定义handler接收子线程传回来的result
myHandler = new MyHandler();
if (resultCode == RESULT_OK){
// 处理照片
try {
// 照片的bitmap
bitmap = BitmapFactory.decodeStream(getActivity().getContentResolver().openInputStream(imageUri));
imageView.setImageBitmap(bitmap);
// 识别
identify(bitmap);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
break;
case PHOTO_REQUEST_ALBUM: // 相册的回调
myHandler = new MyHandler();
try {
Uri selectedImage = data.getData();
String[] filePathColumns = {MediaStore.Images.Media.DATA};
Cursor c = getActivity().getContentResolver().query(selectedImage, filePathColumns, null, null, null);
c.moveToFirst();
int columnIndex = c.getColumnIndex(filePathColumns[0]);
String imagePath = c.getString(columnIndex);
Bitmap bitmap = BitmapFactory.decodeFile(imaePath);
imageView.setImageBitmap(bitmap);
identify(bitmap);
c.close();
}catch (NullPointerException e){
Log.d("E",e.getMessage());
}
break;
}
}
- 先说相机回调:通过ContentValues拿到上文的uri,在变变变成bitmap,传入 identify(bitmap)方法中去识别,在下文百度ai中详细讲这个方法。
- 再谈相册回调:都说是百度的了,前面都没看懂后面怎么可能会,先欠着,大致是一顿操作过后拿到bitmap传入identify()中去,和相机的回调收尾一样。
下面详细讲解identify()
百度ai开放平台
我们先贴上identify()代码
// 识别
String access_token ;
String param;
private void identify(Bitmap bitmap) {
// 压缩到0.1 拿到base64编码,合成param
ByteArrayOutputStream bStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 10, bStream);
byte[] bytes = bStream.toByteArray();
String base64 = Base64Util.encode(bytes);
try {
String image_param = URLEncoder.encode(base64,"UTF-8");
param = "image=" + image_param + "&top_num=" + 6;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// 获得access token 24.1b561ba2365e2ab484e69e953f09a48c.2592000.1555152863.282335-15652740
String request_access_token = "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=aHWDOH5MERuoXkwUIOtbwU7j&client_secret=2QbMhquS9aRY2Gd4hpjlU8SXpIGfC3zm&";
OkHttpClient client = new OkHttpClient.Builder().build();
Request request = new Request.Builder()
.get()
.url(request_access_token)
.build();
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
ToastUtil.toast(getContext(),"access__token请求失败"+e.getMessage());
}
@Override
public void onResponse(Call call, Response response) {
// 解析拿到acces_stoken
String jsonString = null;
try {
jsonString = response.body().string();
} catch (IOException e) {
e.printStackTrace();
}
Log.d("tag",jsonString);
Gson gson = new Gson();
JsonAccess jsonAccess = gson.fromJson(jsonString ,JsonAccess.class);
access_token = jsonAccess.access_token;
}
});
// 开启子线程上传base64
new Thread(new Runnable() {
String url = "https://aip.baidubce.com/rest/2.0/image-classify/v1/animal";
@Override
public void run() {
try {
String result = HttpUtil.post(url,"24.1b561ba2365e2ab484e69e953f09a48c.2592000.1555152863.282335-15652740",param);
// bundle用于传递线程数据
Bundle bundle = new Bundle();
bundle.putString("result",result);
// 通过message发送到主线程
Message message = new Message();
message.setData(bundle);
message.what = 1;
myHandler.sendMessage(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
使用的是百度ai的动物识别接口,官方文档地址:https://ai.baidu.com/docs#/ImageClassify-API/top
在看之前,可以先看一下我的观后感:
- 先注册账号,创建应用,拿到Api key和Secret key。
- 然后看文档
我们传进来的是bitmap,百度需要的base64编码,去掉编码头后再进行urlencode,所以第一步先将我们的bitmap转变为base64,先创建ByteArrayOutputStream流,将bitmap压缩到bStream流里(通过compress方法,参数1为压缩类型,参数2为压缩比例10代表原图片的0.1,参数3为bitmap压缩的目标流),将流转为byte[],通过百度官方提供的Base64转换类的encode方法将byte转为base64,base64转换工具下载地址:https://ai.baidu.com/file/C8D81F3301E24D2892968F09AE1AD6E2
将得到的base64指定为UTF-8,然和合成post请求的param,指定参数image和top_num,下面一段代码是按照官方提示获得accessToken,没什么好说的,(注意我在代码中拿到的accesstoken是在子线程中的,我在下文使用的时候直接传的值,没有传变量,如果传变量需要用到bundle和handler)我们现在所有的操作都是在主线程中,无法进行耗时操作,发送图片为耗时操作,所以我们开启一个线程去post我们的图片给百度,请求返回结果,在子线程中,我们准备好请求的uri地址,accessToken和存放图片的param,通过百度的请求工具获取result,请求工具地址:https://ai.baidu.com/file/544D677F5D4E4F17B4122FBD60DB82B3
现在我们已经在子线程中拿到识别结果了,下面需要将他们传到主线程,解析,展示。
handler与bundle线程间传递数据
先定义MyHandler类用于处理子线程发送来的message
class MyHandler extends Handler{
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
// 取出bundle得到结果
Bundle bundle = msg.getData();
String result = bundle.getString("result");
String s = "";
try {
JSONObject jsonObject = new JSONObject(result);
JSONArray jsonArray = jsonObject.getJSONArray("result");
for (int i = 0; i <1;i++ ){
JSONObject obj = (JSONObject) jsonArray.get(i);
s = s+obj.getString("name")+"\n";
}
textView.setText(s);
} catch (JSONException e) {
e.printStackTrace();
}
textView.setVisibility(View.VISIBLE);
intent_show = new Intent(getContext(),ShowActivity.class);
intent_show.putExtra("result",result);
// getContext().startActivity(intent_show);
}
}
子线程发送数据(上文identify()中的子线程)
new Thread(new Runnable() {
String url = "https://aip.baidubce.com/rest/2.0/image-classify/v1/animal";
@Override
public void run() {
try {
String result = HttpUtil.post(url,"24.1b561ba2365e2ab484e69e953f09a48c.2592000.1555152863.282335-15652740",param);
// bundle用于传递线程数据
Bundle bundle = new Bundle();
bundle.putString("result",result);
// 通过message发送到主线程
Message message = new Message();
message.setData(bundle);
message.what = 1;
myHandler.sendMessage(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
在子线程拿到了返回的result,通过bundle和myHandler传递到主线程,bundle用于存放数据,handler用于发送数据(需要借助handler的message类),在自定义的MyHandler类中我们handleMessage,取出bundle,取出result,解析JSON,然后展示给用户,这些handleMessage的逻辑是写在类里面的,什么时候执行啊,在我们的MyHandler被实例话的时候会执行,仔细去看下上文的onActivityResult在处理相机和相册的两个回调中,我们是不是初始化了我们的myHandler。