百度影像識別
- 運行效果圖
- 一、創建平臺應用
- 二、創建Android專案
- 三、網路訪問框架
- 四、添加請求API介面
- 五、獲取鑒權認證Token
- 六、網路圖片Url識別
- 七、相冊圖片識別
- 八、拍照圖片識別
- 九、原始碼
運行效果圖

如果你對這個效果圖不滿意就不用往下看了,那樣只會浪費你的時間,
一、創建平臺應用
先登錄百度開放平臺

然后進入管理控制臺找到影像識別

點擊進入,

創建應用

然后點擊下方的立即創建按鈕,

注意看下圖示注的資訊

由于影像識別沒有直接的Android SDK,因此本文將通過API訪問進行影像的識別,
二、創建Android專案
通過API方式,則需要先完成鑒權認證,然后拿到Access Token,通過這個Access Token才能去請求這個影像識別的介面,這里要分為兩步走,現在思路清楚了,下面先創建一個專案吧,命名為ImageDiscernDemo,

專案創建好之后,先配置專案,打開工程的build.gradle,添加如下代碼:
maven { url "https://jitpack.io" }
添加位置如下:

然后是修改app下的build.gradle,有兩處
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
//權限請求框架
implementation 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
implementation "io.reactivex.rxjava2:rxjava:2.0.0"
//retrofit2
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.4.1'
//RecyclerView最好的配接器,讓你的配接器一目了然,告別代碼冗余
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.30'
//Glide框架
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
//添加material庫
implementation 'com.google.android.material:material:1.2.1'
添加位置如下:


然后Sync Now,完成后來配置AndroidManifest.xml,添加如下權限:
<!--網路權限-->
<uses-permission android:name="android.permission.INTERNET"/>
<!--檔案讀寫-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!--相機-->
<uses-permission android:name="android.permission.CAMERA"/>

再來配置一個FileProvider,在layout下新建一個xml檔案,檔案夾下新建一個file_paths.xml,里面的代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="" />
</paths>
然后在AndroidManifest.xml中配置
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.llw.imagediscerndemo.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
添加位置如下圖:

下面我們首先簡單來寫一個網路訪問的工具類,
三、網路訪問框架
在com.llw.imagediscerndemo下新建一個network包,包下新建一個ServiceGenerator類,里面的代碼如下:
package com.llw.imagediscerndemo.network;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
/**
* 介面地址管理
*
* @author llw
*/
public class ServiceGenerator {
/**
* 默認地址
*/
public static String BASE_URL = "https://aip.baidubce.com";
/**
* 創建服務 引數就是API服務
*
* @param serviceClass 服務介面
* @param <T> 泛型規范
* @return api介面服務
*/
public static <T> T createService(Class<T> serviceClass) {
//創建OkHttpClient構建器物件
OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder();
//設定請求超時的時間,這里是10秒
okHttpClientBuilder.connectTimeout(20000, TimeUnit.MILLISECONDS);
//訊息攔截器 因為有時候介面不同在排錯的時候 需要先從介面的回應中做分析,利用了訊息攔截器可以清楚的看到介面回傳的所有內容
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor();
//setlevel用來設定日志列印的級別,共包括了四個級別:NONE,BASIC,HEADER,BODY
//BASEIC:請求/回應行
//HEADER:請求/回應行 + 頭
//BODY:請求/回應航 + 頭 + 體
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
//為OkHttp添加訊息攔截器
okHttpClientBuilder.addInterceptor(httpLoggingInterceptor);
//在Retrofit中設定httpclient
//設定地址 就是上面的固定地址,如果你是本地訪問的話,可以拼接上埠號 例如 +":8080"
Retrofit retrofit = new Retrofit.Builder().baseUrl(BASE_URL)
//用Gson把服務端回傳的json資料決議成物體
.addConverterFactory(GsonConverterFactory.create())
//放入OKHttp,之前說過retrofit是對OkHttp的進一步封裝
.client(okHttpClientBuilder.build())
.build();
//回傳這個創建好的API服務
return retrofit.create(serviceClass);
}
}
很簡單的代碼,也都是網路上常見的,OkHttp + Retrofit,
這里面的默認地址 https://aip.baidubce.com是影像識別API的固定地址,后面的有變化的,通過介面來配置,
然后再寫一個NetCallBack類,用來處理Retrofit的回傳,里面的代碼如下:
package com.llw.imagediscerndemo.network;
import android.util.Log;
import com.google.gson.Gson;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* 網路請求回呼
*
* @param <T>
*/
public abstract class NetCallBack<T> implements Callback<T> {//這里實作了retrofit2.Callback
//訪問成功回呼
@Override
public void onResponse(Call<T> call, Response<T> response) {//資料回傳
if (response != null && response.body() != null && response.isSuccessful()) {
onSuccess(call, response);
} else {
onFailed(response.raw().toString());
}
}
//訪問失敗回呼
@Override
public void onFailure(Call<T> call, Throwable t) {
Log.d("data str", t.toString());
onFailed(t.toString());
}
//資料回傳
public abstract void onSuccess(Call<T> call, Response<T> response);
//失敗例外
public abstract void onFailed(String errorStr);
}
那么這樣簡單的網路框架寫好了,
四、添加請求API介面
百度的影像識別,首先要完成鑒權認證,拿到一個Token,然后通過這個Token再去請求影像識別的API介面才行,所以要完成兩步操作,先來看第一步
鑒權的地址如下:
https://aip.baidubce.com/oauth/2.0/token
這里面需要再帶三個Url引數,使用Post請求方式,
grant_type: 必須引數,固定為client_credentials;
client_id: 必須引數,應用的API Key;
client_secret: 必須引數,應用的Secret Key;
回傳的是一串JSON字串,如下:
{
"refresh_token": "25.b55fe1d287227ca97aab219bb249b8ab.315360000.1798284651.282335-8574074",
"expires_in": 2592000,
"scope": "public wise_adapt",
"session_key": "9mzdDZXu3dENdFZQurfg0Vz8slgSgvvOAUebNFzyzcpQ5EnbxbF+hfG9DQkpUVQdh4p6HbQcAiz5RmuBAja1JJGgIdJI",
"access_token": "24.6c5e1ff107f0e8bcef8c46d3424a0e78.2592000.1485516651.282335-8574074",
"session_secret": "dfac94a3489fe9fca7c3221cbf7525ff"
}
當請求和回傳都確定之后,我們就可以構建請求物體和回傳物體了,
而請求物體都是放在Url中的,因此不需要通過物體來構建,直接傳引數就好,
在com.llw.imagediscerndemo包下新建一個model包,包下新建一個GetTokenResponse類,里面的代碼如下:
package com.llw.imagediscerndemo.model;
/**
* 獲取鑒權認證Token回應物體
*/
public class GetTokenResponse {
/**
* refresh_token : 25.b55fe1d287227ca97aab219bb249b8ab.315360000.1798284651.282335-8574074
* expires_in : 2592000
* scope : public wise_adapt
* session_key : 9mzdDZXu3dENdFZQurfg0Vz8slgSgvvOAUebNFzyzcpQ5EnbxbF+hfG9DQkpUVQdh4p6HbQcAiz5RmuBAja1JJGgIdJI
* access_token : 24.6c5e1ff107f0e8bcef8c46d3424a0e78.2592000.1485516651.282335-8574074
* session_secret : dfac94a3489fe9fca7c3221cbf7525ff
*/
private String refresh_token;
private long expires_in;
private String scope;
private String session_key;
private String access_token;
private String session_secret;
public String getRefresh_token() {
return refresh_token;
}
public void setRefresh_token(String refresh_token) {
this.refresh_token = refresh_token;
}
public long getExpires_in() {
return expires_in;
}
public void setExpires_in(long expires_in) {
this.expires_in = expires_in;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public String getSession_key() {
return session_key;
}
public void setSession_key(String session_key) {
this.session_key = session_key;
}
public String getAccess_token() {
return access_token;
}
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
public String getSession_secret() {
return session_secret;
}
public void setSession_secret(String session_secret) {
this.session_secret = session_secret;
}
}
這是通過剛才的回傳資料生成的物體Bean,當網路請求回傳資料后通過Retrofit會決議成這個回傳物體,
下面添加介面,在network包下新建一個ApiService介面,里面的代碼如下:
package com.llw.imagediscerndemo.network;
import com.llw.imagediscerndemo.model.GetTokenResponse;
import retrofit2.Call;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.Headers;
import retrofit2.http.POST;
/**
* API服務
*
* @author llw
* @date 2021/4/1 17:48
*/
public interface ApiService {
/**
* 獲取鑒權認證Token
* @param grant_type 型別
* @param client_id API Key
* @param client_secret Secret Key
* @return GetTokenResponse
*/
@FormUrlEncoded
@POST("/oauth/2.0/token")
Call<GetTokenResponse> getToken(@Field("grant_type") String grant_type,
@Field("client_id") String client_id,
@Field("client_secret") String client_secret);
}
這里還有一個介面呢,那就是影像識別介面,地址為:
https://aip.baidubce.com/rest/2.0/image-classify/v2/advanced_general
找個介面比較特殊,官方檔案的描述如下:

看你是否理解了,回傳資料如下:
{
"log_id": 327863200205075661,
"result_num": 5,
"result": [{
"score": 0.967622,
"root": "公眾人物",
"baike_info": {
"baike_url": "http://baike.baidu.com/item/%E6%96%B0%E5%9E%A3%E7%BB%93%E8%A1%A3/8035884",
"image_url": "http://imgsrc.baidu.com/baike/pic/item/91ef76c6a7efce1b27893518a451f3deb58f6546.jpg",
"description": "新垣結衣(Aragaki Yui),1988年6月11日出生于沖繩縣那霸市,日本女演員、歌手、模特,畢業于日出高中,2001年,參加《nicola》模特比賽并獲得最優秀獎,2005年,因出演現代劇《澀谷15》而作為演員出道,2006年,參演校園劇《我的老大,我的英雄》;同年,她還出版了個人首本寫真集《水漾青春》,2007年,她從日出高校畢業后開始專注于演藝發展,并發表個人首張音樂專輯《天空》;同年,新垣結衣還主演了愛情片《戀空》,而她也憑借該片獲得了多個電影新人獎項,2010年,主演愛情片《花水木》,2011年,主演都市劇《全開女孩》,2012年,相繼參演現代劇《Legal High》、劇情片《劇場版新參者:麒麟之翼》,2013年,主演都市劇《飛翔情報室》,2014年,她主演了劇情片《黎明的沙耶》,2016年,主演愛情喜劇《逃避雖可恥但有用》,并憑借該劇獲得了多個電視劇女主角獎項,2017年,主演愛情片《戀愛回旋》,憑借該片獲得第60屆藍絲帶獎最佳女主角;同年11月,她還憑借醫療劇《Code Blue 3》獲得第94屆日劇學院賞最佳女配角,"
},
"keyword": "新垣結衣"
},
{
"score": 0.716067,
"root": "人物-人物特寫",
"keyword": "頭發"
},
{
"score": 0.421281,
"root": "商品-穿戴",
"keyword": "圍巾"
},
{
"score": 0.22347,
"root": "商品-五金",
"keyword": "拉鏈"
},
{
"score": 0.028031,
"root": "商品-穿戴",
"keyword": "脖套"
}]
}
通過這個回傳示例資料,可以生成一個物體Bean,在model包下新建一個GetDiscernResultResponse類,代碼如下:
package com.llw.imagediscerndemo.model;
import java.util.List;
/**
* 獲取識別結果回應物體
*/
public class GetDiscernResultResponse {
/**
* log_id : 327863200205075661
* result_num : 5
* result : [{"score":0.967622,"root":"公眾人物","baike_info":{"baike_url":"http://baike.baidu.com/item/%E6%96%B0%E5%9E%A3%E7%BB%93%E8%A1%A3/8035884","image_url":"http://imgsrc.baidu.com/baike/pic/item/91ef76c6a7efce1b27893518a451f3deb58f6546.jpg","description":"新垣結衣(Aragaki Yui),1988年6月11日出生于沖繩縣那霸市,日本女演員、歌手、模特,畢業于日出高中,2001年,參加《nicola》模特比賽并獲得最優秀獎,2005年,因出演現代劇《澀谷15》而作為演員出道,2006年,參演校園劇《我的老大,我的英雄》;同年,她還出版了個人首本寫真集《水漾青春》,2007年,她從日出高校畢業后開始專注于演藝發展,并發表個人首張音樂專輯《天空》;同年,新垣結衣還主演了愛情片《戀空》,而她也憑借該片獲得了多個電影新人獎項,2010年,主演愛情片《花水木》,2011年,主演都市劇《全開女孩》,2012年,相繼參演現代劇《Legal High》、劇情片《劇場版新參者:麒麟之翼》,2013年,主演都市劇《飛翔情報室》,2014年,她主演了劇情片《黎明的沙耶》,2016年,主演愛情喜劇《逃避雖可恥但有用》,并憑借該劇獲得了多個電視劇女主角獎項,2017年,主演愛情片《戀愛回旋》,憑借該片獲得第60屆藍絲帶獎最佳女主角;同年11月,她還憑借醫療劇《Code Blue 3》獲得第94屆日劇學院賞最佳女配角,"},"keyword":"新垣結衣"},{"score":0.716067,"root":"人物-人物特寫","keyword":"頭發"},{"score":0.421281,"root":"商品-穿戴","keyword":"圍巾"},{"score":0.22347,"root":"商品-五金","keyword":"拉鏈"},{"score":0.028031,"root":"商品-穿戴","keyword":"脖套"}]
*/
private long log_id;
private int result_num;
private List<ResultBean> result;
public long getLog_id() {
return log_id;
}
public void setLog_id(long log_id) {
this.log_id = log_id;
}
public int getResult_num() {
return result_num;
}
public void setResult_num(int result_num) {
this.result_num = result_num;
}
public List<ResultBean> getResult() {
return result;
}
public void setResult(List<ResultBean> result) {
this.result = result;
}
public static class ResultBean {
/**
* score : 0.967622
* root : 公眾人物
* baike_info : {"baike_url":"http://baike.baidu.com/item/%E6%96%B0%E5%9E%A3%E7%BB%93%E8%A1%A3/8035884","image_url":"http://imgsrc.baidu.com/baike/pic/item/91ef76c6a7efce1b27893518a451f3deb58f6546.jpg","description":"新垣結衣(Aragaki Yui),1988年6月11日出生于沖繩縣那霸市,日本女演員、歌手、模特,畢業于日出高中,2001年,參加《nicola》模特比賽并獲得最優秀獎,2005年,因出演現代劇《澀谷15》而作為演員出道,2006年,參演校園劇《我的老大,我的英雄》;同年,她還出版了個人首本寫真集《水漾青春》,2007年,她從日出高校畢業后開始專注于演藝發展,并發表個人首張音樂專輯《天空》;同年,新垣結衣還主演了愛情片《戀空》,而她也憑借該片獲得了多個電影新人獎項,2010年,主演愛情片《花水木》,2011年,主演都市劇《全開女孩》,2012年,相繼參演現代劇《Legal High》、劇情片《劇場版新參者:麒麟之翼》,2013年,主演都市劇《飛翔情報室》,2014年,她主演了劇情片《黎明的沙耶》,2016年,主演愛情喜劇《逃避雖可恥但有用》,并憑借該劇獲得了多個電視劇女主角獎項,2017年,主演愛情片《戀愛回旋》,憑借該片獲得第60屆藍絲帶獎最佳女主角;同年11月,她還憑借醫療劇《Code Blue 3》獲得第94屆日劇學院賞最佳女配角,"}
* keyword : 新垣結衣
*/
private double score;
private String root;
private BaikeInfoBean baike_info;
private String keyword;
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
public String getRoot() {
return root;
}
public void setRoot(String root) {
this.root = root;
}
public BaikeInfoBean getBaike_info() {
return baike_info;
}
public void setBaike_info(BaikeInfoBean baike_info) {
this.baike_info = baike_info;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
public static class BaikeInfoBean {
/**
* baike_url : http://baike.baidu.com/item/%E6%96%B0%E5%9E%A3%E7%BB%93%E8%A1%A3/8035884
* image_url : http://imgsrc.baidu.com/baike/pic/item/91ef76c6a7efce1b27893518a451f3deb58f6546.jpg
* description : 新垣結衣(Aragaki Yui),1988年6月11日出生于沖繩縣那霸市,日本女演員、歌手、模特,畢業于日出高中,2001年,參加《nicola》模特比賽并獲得最優秀獎,2005年,因出演現代劇《澀谷15》而作為演員出道,2006年,參演校園劇《我的老大,我的英雄》;同年,她還出版了個人首本寫真集《水漾青春》,2007年,她從日出高校畢業后開始專注于演藝發展,并發表個人首張音樂專輯《天空》;同年,新垣結衣還主演了愛情片《戀空》,而她也憑借該片獲得了多個電影新人獎項,2010年,主演愛情片《花水木》,2011年,主演都市劇《全開女孩》,2012年,相繼參演現代劇《Legal High》、劇情片《劇場版新參者:麒麟之翼》,2013年,主演都市劇《飛翔情報室》,2014年,她主演了劇情片《黎明的沙耶》,2016年,主演愛情喜劇《逃避雖可恥但有用》,并憑借該劇獲得了多個電視劇女主角獎項,2017年,主演愛情片《戀愛回旋》,憑借該片獲得第60屆藍絲帶獎最佳女主角;同年11月,她還憑借醫療劇《Code Blue 3》獲得第94屆日劇學院賞最佳女配角,
*/
private String baike_url;
private String image_url;
private String description;
public String getBaike_url() {
return baike_url;
}
public void setBaike_url(String baike_url) {
this.baike_url = baike_url;
}
public String getImage_url() {
return image_url;
}
public void setImage_url(String image_url) {
this.image_url = image_url;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
}
}
下面在ApiService中添加介面,
/**
* 獲取影像識別結果
* @param accessToken 獲取鑒權認證Token
* @param url 網路圖片Url
* @return JsonObject
*/
@FormUrlEncoded
@POST("/rest/2.0/image-classify/v2/advanced_general")
@Headers("Content-Type:application/x-www-form-urlencoded; charset=utf-8")
Call<GetDiscernResultResponse> getDiscernResult(@Field("access_token") String accessToken,
@Field("url") String url);
我這個寫法和官方說明好像有點不同,我沒有用Body,選擇直接把資料放在Url中請求,
現在萬事具備了,下面先來獲取鑒權認證Token,
五、獲取鑒權認證Token
打開MainActivity,添加如下代碼:
private static final String TAG = "MainActivity";
/**
* Api服務
*/
private ApiService service;
/**
* 鑒權Toeken
*/
private String accessToken;
然后在onCreate方法中對ApiService進行實體化,
service = ServiceGenerator.createService(ApiService.class);
新增如下方法獲取Token,
/**
* 訪問API獲取介面
*/
private void requestApiGetToken() {
String grantType = "client_credentials";
String apiKey = "TjPChftoEyBq7Nzm65KNerqr";
String apiSecret = "eTph4jO95te6R3G2aecktGMbkieOv7rS";
service.getToken(grantType, apiKey, apiSecret)
.enqueue(new NetCallBack<GetTokenResponse>() {
@Override
public void onSuccess(Call<GetTokenResponse> call, Response<GetTokenResponse> response) {
if (response.body() != null) {
//鑒權Token
accessToken = response.body().getAccess_token();
Log.d(TAG,accessToken);
}
}
@Override
public void onFailed(String errorStr) {
Log.e(TAG, "獲取Token失敗,失敗原因:" + errorStr);
accessToken = null;
}
});
}
然后在onCreate中呼叫它,

運行一下,看一下控制臺是否列印了日志,

你可以看到這個Token還是挺長的,對于這個Token,是有有效期的,基本上是一個月,所以你可以不用每次使用時都重新請求這個介面去獲取Token,這里可以用快取來解決這個問題,
說一下邏輯,當通過介面拿到Token時保存Token、Token獲取時間、Token有效時長三個資料到快取中,每一次使用前進行一次判斷,首先是判斷有沒有Token,其次是判斷Token有沒有過期,那么按照這個思路我們就可以這么寫代碼了,
這里為了方便我在com.llw.imagediscerndemo包下新建一個util包,包下新建一個Constant類,里面的代碼如下:
package com.llw.imagediscerndemo.util;
/**
* 全域常量
*/
public class 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";
}
這三個值,我剛才也說明過了,下面寫一個快取的SPUtils工具類,里面的代碼如下:
package com.llw.imagediscerndemo.util;
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();
}
}
也是很簡單的代碼,相信你一眼就看明白了,下面就該在MainActivity中去處理快取資料的存取了,
首先是放快取,這當然是在請求介面的成功資料回傳中放,修改onSuccess中的代碼,如下,
@Override
public void onSuccess(Call<GetTokenResponse> call, Response<GetTokenResponse> response) {
if (response.body() != null) {
//鑒權Token
accessToken = response.body().getAccess_token();
//過期時間 秒
long expiresIn = response.body().getExpires_in();
//當前時間 秒
long currentTimeMillis = System.currentTimeMillis() / 1000;
//放入快取
SPUtils.putString(Constant.TOKEN, accessToken, MainActivity.this);
SPUtils.putLong(Constant.GET_TOKEN_TIME, currentTimeMillis, MainActivity.this);
SPUtils.putLong(Constant.TOKEN_VALID_PERIOD, expiresIn, MainActivity.this);
}
}
然后寫一個判斷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的時間,然后獲取Token的有效時長,再獲取當前系統時間,然后通過當前系統時間減去獲得Token的時間,得到的值再與Token有效期做比較,如果大于等于有效期則說明Token過期,回傳true,否則回傳false,
下面再寫一個方法,用來獲取Token,同時將我們之前寫的代碼給串起來,
/**
* 獲取鑒權Token
*/
private String getAccessToken() {
String token = SPUtils.getString(Constant.TOKEN, null, this);
if (token == null) {
//訪問API獲取介面
requestApiGetToken();
} else {
//則判斷Token是否過期
if (isTokenExpired()) {
//過期
requestApiGetToken();
} else {
accessToken = token;
}
}
return accessToken;
}
首先獲取快取中的Token,應用第一次進入肯定是沒有值的,沒有值則回傳默認值null,那么token變數此時為null,那么就會通過介面去獲取Token,當獲取之后存入快取,再次進入時,就不是null了,那么就會通過isTokenExpired()方法來判斷Token是否過期,過期了也是通過網路請求重新拿到Token放入快取,如果沒有過期則直接使用快取中的Token,最后回傳Token,
六、網路圖片Url識別
Token拿到以后我們來進行網路圖片Url識別,先說一下思路,首先是通過網路圖片url和Token去請求介面,然后獲得回傳值,此時要顯示一個加載條,然后通過回傳資料渲染串列,當資料顯示在串列之后就完成了,
首先找一個網路圖片Url,如下:
https://bce-baiyu.cdn.bcebos.com/14ce36d3d539b6004ef2e45fe050352ac65cb71e.jpeg
這個網路圖片是一個水杯的圖片,如下所示:

首先修改布局activity_main.xml,里面的代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".MainActivity">
<ImageView
android:id="@+id/iv_picture"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop" />
<LinearLayout
android:layout_marginBottom="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="IdentifyWebPictures"
android:text="識別網路圖片" />
</LinearLayout>
<ProgressBar
android:visibility="gone"
android:id="@+id/pb_loading"
android:layout_centerInParent="true"
android:layout_width="60dp"
android:layout_height="60dp"/>
</RelativeLayout>
然后在MainActivity中寫入IdentifyWebPictures方法,代碼如下:
/**
* 識別網路圖片
*
* @param view
*/
public void IdentifyWebPictures(View view) {
}
首先創建物件
/**
* 顯示圖片
*/
private ImageView ivPicture;
/**
* 進度條
*/
private ProgressBar pbLoading;
然后在onCreate中系結xml中的控制元件id,
ivPicture = findViewById(R.id.iv_picture);
pbLoading = findViewById(R.id.pb_loading);
下面來修改IdentifyWebPictures()方法的代碼,如下:
public void IdentifyWebPictures(View view) {
pbLoading.setVisibility(View.VISIBLE);
String token = getAccessToken();
String imgUrl = "https://bce-baiyu.cdn.bcebos.com/14ce36d3d539b6004ef2e45fe050352ac65cb71e.jpeg";
//顯示圖片
Glide.with(this).load(imgUrl).into(ivPicture);
showMsg("影像識別中");
service.getDiscernResult(token, imgUrl).enqueue(new NetCallBack<GetDiscernResultResponse>() {
@Override
public void onSuccess(Call<GetDiscernResultResponse> call, Response<GetDiscernResultResponse> response) {
List<GetDiscernResultResponse.ResultBean> result = response.body() != null ? response.body().getResult() : null;
if (result != null && result.size() > 0) {
//顯示識別結果
showDiscernResult(result);
} else {
pbLoading.setVisibility(View.GONE);
showMsg("未獲得相應的識別結果");
}
}
@Override
public void onFailed(String errorStr) {
pbLoading.setVisibility(View.GONE);
Log.e(TAG, "影像識別失敗,失敗原因:" + errorStr);
}
});
}
當點擊按鈕時,顯示進度條,然后通過getAccessToken()方法獲取Token,之后顯示網路圖片在ImageView控制元件中,Toast提示一下,之后請求的成功和失敗的回呼了,在成功的回呼中先判斷資料是否為空,不為空再通過showDiscernResult()方法去顯示資料,下面寫這個方法,
/**
* 顯示識別的結果串列
*
* @param result
*/
private void showDiscernResult(List<GetDiscernResultResponse.ResultBean> result) {
}
showMsg方法:
/**
* Toast提示
* @param msg 內容
*/
private void showMsg(String msg){
Toast.makeText(this,msg,Toast.LENGTH_SHORT).show();
}
為了不占用螢屏的控制元件,我這里打算用一個彈窗來顯示資料,彈窗里面是一個串列,串列通過item布局構建,資料由剛才的方法傳遞進來,我們一步一步來寫,首先構建item的布局,在layout下新建一個item_result_rv.xml,里面的代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:padding="16dp"
android:background="#FFF"
android:layout_marginBottom="1dp"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_keyword"
android:textSize="16sp"
android:textColor="#000"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_marginTop="@dimen/dp_4"
android:layout_below="@+id/tv_keyword"
android:id="@+id/tv_root"
android:textSize="14sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/tv_score"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
item布局有了,下面構建彈窗的布局代碼,在layout下新建一個 dialog_bottom.xml,里面的代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#EEE"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFF"
android:gravity="center"
android:padding="16dp"
android:text="識別結果" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="1dp" />
</LinearLayout>
下面布局都有了,先構建這個串列的配接器,在com.llw.imagediscerndemo下新建一個adapter包,包下新建一個DiscernResultAdapter類,里面的代碼如下:
package com.llw.imagediscerndemo.adapter;
import androidx.annotation.Nullable;
import com.chad.library.adapter.base.BaseQuickAdapter;
import com.chad.library.adapter.base.BaseViewHolder;
import com.llw.imagediscerndemo.R;
import com.llw.imagediscerndemo.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()));
}
}
萬事具備,只差顯示資料了,下面進入MainActivity中,首先創建物件
/**
* 底部彈窗
*/
private BottomSheetDialog bottomSheetDialog;
/**
* 彈窗視圖
*/
private View bottomView;
然后在onCreate中實體化,
bottomSheetDialog = new BottomSheetDialog(this);
bottomView = getLayoutInflater().inflate(R.layout.dialog_bottom, null);
然后修改showDiscernResult方法,代碼如下:
private void showDiscernResult(List<GetDiscernResultResponse.ResultBean> result) {
bottomSheetDialog.setContentView(bottomView);
bottomSheetDialog.getWindow().findViewById(R.id.design_bottom_sheet).setBackgroundColor(Color.TRANSPARENT);
RecyclerView rvResult = bottomView.findViewById(R.id.rv_result);
DiscernResultAdapter adapter = new DiscernResultAdapter(R.layout.item_result_rv, result);
rvResult.setLayoutManager(new LinearLayoutManager(this));
rvResult.setAdapter(adapter);
//隱藏加載
pbLoading.setVisibility(View.GONE);
//顯示彈窗
bottomSheetDialog.show();
}
下面運行一下:

可以看到結果識別到了,
七、相冊圖片識別
在實際應用中,更多是采用本地的圖片進行識別,通常是選擇拍照的圖片或者打開相冊獲取圖片,先來看看通過相冊獲取圖片進行影像識別,要實作這個功能首先要改一下介面,加一個image引數,

然后修改ImageDiscern方法,
/**
* 影像識別請求
*
* @param token token
* @param imageBase64 圖片Base64
* @param imgUrl 網路圖片Url
*/
private void ImageDiscern(String token, String imageBase64, String imgUrl) {
service.getDiscernResult(token, imageBase64, imgUrl).enqueue(new NetCallBack<GetDiscernResultResponse>() {
@Override
public void onSuccess(Call<GetDiscernResultResponse> call, Response<GetDiscernResultResponse> response) {
List<GetDiscernResultResponse.ResultBean> result = response.body() != null ? response.body().getResult() : null;
if (result != null && result.size() > 0) {
//顯示識別結果
showDiscernResult(result);
} else {
pbLoading.setVisibility(View.GONE);
showMsg("未獲得相應的識別結果");
}
}
@Override
public void onFailed(String errorStr) {
pbLoading.setVisibility(View.GONE);
Log.e(TAG, "影像識別失敗,失敗原因:" + errorStr);
}
});
}
這個方法接收三個引數,Token、ImageBase64、圖片Url,ImageBase64和圖片Url只能二選一,選其中一個另一個則傳null,比如之前的通過網路圖片Url識別,

介面的相關方法都改好了,下面來寫打開相冊的方法,Android6.0以后讀寫檔案都屬于危險權限,因此需要動態請求,在MainActivity中宣告:
private RxPermissions rxPermissions;
然后在onCreate中實體化
rxPermissions = new RxPermissions(this);
下面修改布局,在之前的按鈕后面再加一個按鈕
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="IdentifyAlbumPictures"
android:text="識別相冊圖片" />
然后在MainActivity中增加IdentifyAlbumPictures方法,代碼如下:
/**
* 識別相冊圖片
*
* @param view
*/
@SuppressLint("CheckResult")
public void IdentifyAlbumPictures(View view) {
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();
}
}
當獲取到權限之后通過openAlbum()方法打開相冊,openAlbum方法代碼如下:
/**
* 打開相冊
*/
private void openAlbum() {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_PICK);
intent.setType("image/*");
startActivityForResult(intent, OPEN_ALBUM_CODE);
}
這里定義了一個請求碼
/**
* 打開相冊
*/
private static final int OPEN_ALBUM_CODE = 100;
打開相冊之后就要回傳了,重寫 onActivityResult方法
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
pbLoading.setVisibility(View.VISIBLE);
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("什么都沒有");
}
}
相冊回傳之后先拿到圖片的Uri,然后通過Uri得到圖片的路徑,然后通過這個路徑將圖片轉成位元組,再轉Base64,首先來看localImageDiscern方法,代碼如下:
/**
* 本地圖片識別
*/
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);
//影像識別
ImageDiscern(token, imageBase64, null);
} catch (IOException e) {
e.printStackTrace();
}
}
這里面有兩個工具類FileUtil和Base64Util,代碼如下:
FileUtil.java
package com.llw.imagediscerndemo.util;
import java.io.*;
/**
* 檔案讀取工具類
*/
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.java
package com.llw.imagediscerndemo.util;
/**
* 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();
}
}
都放在util包下,那么現在就可以直接運行了,

通過這個圖可以看到第一次識別失敗了,第二次成功了,后續的都會成功,不知道是什么奇葩原因,有知道的記得告訴我啊,
八、拍照圖片識別
首先還在在activity_main.xml中識別相冊圖片按鈕的后面加一個識別拍照圖片按鈕,如下:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="IdentifyTakePhotoImage"
android:text="識別拍照圖片" />
在MainActivity中增加IdentifyTakePhotoImage方法,代碼如下:
/**
* 識別拍照圖片
*
* @param view
*/
@SuppressLint("CheckResult")
public void IdentifyTakePhotoImage(View view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
rxPermissions.request(
Manifest.permission.CAMERA)
.subscribe(grant -> {
if (grant) {
//獲得權限
turnOnCamera();
} else {
showMsg("未獲取到權限");
}
});
} else {
turnOnCamera();
}
}
來看看turnOnCamera方法,在此之前創建變數,用來保存拍照后的圖片
private File outputImage;
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.imagediscerndemo.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);
}
這里同樣配置了一個打開相機的請求碼
/**
* 打開相機
*/
private static final int TAKE_PHOTO_CODE = 101;
下面進入到onActivityResult方法,加一個條件分支,

通過這個圖片保存檔案得到圖片的路徑,然后通過localImageDiscern()方法對這個路徑下的檔案進行處理,和打開相冊之后拿到路徑之后呼叫的是同一個方法,下面來運行一下:

嗯,那么到這里整個Demo就寫完了,是不是還挺簡單的,只要思路明確再加上細節處理的到位,任何的功能都不在話下,對吧,
九、原始碼
考慮到有時候GitHub會抽風,因此加上了CSDN的資源下載地址,我設定的0積分,隨便下載,
如果你用的好,不妨給我的原始碼或者資源點個贊給個好評啥的,
GitHub原始碼地址:ImageDiscernDemo
CSDN資源地址:ImageDiscernDemo.rar
掃碼下載APK使用:

我是初學者-Study,山高水長,后會有期~
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/273266.html
標籤:其他
