虹软人脸识别SDK1.2版本是免费的,但是官方提供的Demo是离线版本的,人脸数据保存在手机上,换一部手机就无法识别。本文基于其进行Android版本的人脸识别功能、性别识别功能、年龄识别功能开发,并在Java后端建立人脸库,统一管理人脸识别数据,做到A手机上注册,在B手机上也能识别的功能。
一、下载官方提供的Demo
地址为 https://github.com/asdfqwrasdf/ArcFaceDemo,下载完后解压,使用Android Studio打开
二、下面是注册各种ID值和下载SDK,实现官方Demo的离线人脸识别功能:
第二步:去虹软人脸识别平台:https://ai.arcsoft.com.cn/ 进行注册、登录,然后进行个人实名认证。不实名认证的话有的功能会被限制使用
第三步:如下新建应用:
第四步:选择功能为人脸识别:
第五步:填写信息:
第六步:注意下图的的APP ID和SDK KEY,后面要复制到Demo中去
第七步:下载上图中的SDK,下载下来一共有五个压缩包,每一个都要解压,并把每一个压缩包中的libs中的东西复制到Demo项目中的libs中去,其中五个压缩包中的armeabi下的libmpbase.so都是一样的,复制时选择覆盖,同理五个包中的armeabi-v7a下的libmpbase.so也是一样的,复制时选择覆盖:
五个压缩包如下:
复制到Demo项目中后,项目如下:
第八步、将官方平台上的各种key和id复制到FaceDB类中,对应如下:
/**
* APP ID
*/
public static String appid = "xxx";
/**
* 人脸追踪(FT)
*/
public static String ft_key = "xxx";
/**
* 人脸检测(FD)
*/
public static String fd_key = "xxx";
/**
* 人脸识别(FR)
*/
public static String fr_key = "xxx";
/**
* 年龄识别(Age)
*/
public static String age_key = "xxx";
/**
* 性别识别(Gender)
*/
public static String gender_key
运行APP,先点击注册人脸数据,并为注册人脸图片命名,然后进行识别,就能看到置信度分数(即匹配程度分数)。当这个分数大于0.6时(这个值可以自己调整),说明识别成功
三、改造官方Demo,实现人脸数据上传到服务器并从服务器同步
要实现这个功能,主要改造的是Demo中的FaceDB类,因为人脸注册的时候,数据是通过这个类保存在SD卡上的,人脸识别的时候,数据也是通过这个类从SD卡加载的,改造后如下:注意复制官网提供的各种Key和ID值到本类中;另外注意人脸数据上传下载的两个链接为Java后端提供的链接
/**
* 人脸识别数据操作相关
*/
public class FaceDB
{
private final String TAG = this.getClass().toString();
//以下几个id和key是从虹软人脸识别平台(https://ai.arcsoft.com.cn/)申请的。使用的是V1.2版本
/**
* APP ID
*/
public static String appid = "xxx";
/**
* 人脸追踪(FT)
*/
public static String ft_key = "xxx";
/**
* 人脸检测(FD)
*/
public static String fd_key = "xxx";
/**
* 人脸识别(FR)
*/
public static String fr_key = "xxx";
/**
* 年龄识别(Age)
*/
public static String age_key = "xxx";
/**
* 性别识别(Gender)
*/
public static String gender_key = "xxx";
/**
* 人脸数据存储在本地SD卡的路径
*/
String mDBPath;
/**
* 人脸注册数据集合:FaceRegist是一个DataBean,其中包含注册图片的名称和保存人脸特征信息的AFR_FSDKFace的Map集合
*/
List<FaceRegist> mRegister;
/**
* 这个类具体实现了人脸识别的功能
*/
AFR_FSDKEngine mFREngine;
/**
* 这个类用来保存版本信息
*/
AFR_FSDKVersion mFRVersion;
/**
* 是否更新用户数据
*/
boolean mUpgrade;
/**
* 人脸注册信息DataBean
*/
class FaceRegist
{
/**
* 注册的图片名称
*/
String mName;
/**
* 人脸特征信息集合。第一个泛型是图片链接,第二个泛型AFR_FSDKFace: 这个类用来保存人脸特征信息
*/
Map<String, AFR_FSDKFace> mFaceList;
/**
* 构造方法中设置图片名称
*
* @param name : 图片名称
*/
public FaceRegist(String name)
{
mName = name;
mFaceList = new LinkedHashMap<>();
}
}
/**
* 人脸数据上传链接。需要上传face.txt和xxx.data
*/
private String uploadUrl = "http://localhost:10086/upload";
/**
* 人脸数据下载链接。需要下载face.txt和xxx.data
*/
private String downloadUrl = "http://localhost:10086/download?fileName=";
/***
* 文件上传下载工具
*/
private FaceFileUtils faceFileUtils;
/**
* 本类构造方法:其中初始化FaceDB相关数据
*
* @param path :数据存储路径
*/
public FaceDB(String path)
{
mDBPath = path;
mRegister = new ArrayList<>();
mFRVersion = new AFR_FSDKVersion();
mUpgrade = false;
mFREngine = new AFR_FSDKEngine();
/**AFR_FSDKError: 这个类用来保存错误信息*/
AFR_FSDKError error = mFREngine.AFR_FSDK_InitialEngine(FaceDB.appid, FaceDB.fr_key);
if (error.getCode() != AFR_FSDKError.MOK) //MOK: 成功
{//如果AFR_FSDKEngine初始化失败
Log.e(TAG, "AFR_FSDKEngine初始化失败! 错误码为:" + error.getCode());
} else
{//如果AFR_FSDKEngine初始化成功
mFREngine.AFR_FSDK_GetVersion(mFRVersion);
Log.d(TAG, "AFR_FSDKEngine初始化成功,版本号为:" + mFRVersion.toString());
}
//创建文件上传下载工具实例
faceFileUtils = new FaceFileUtils(mDBPath, uploadUrl, downloadUrl);
//程序启动时,先同步下载服务器上的face.txt和所有的xxx.data数据
faceFileUtils.downloadFile();
}
/**
* 销毁AFR_FSDKEngine
*/
public void destroy()
{
if (mFREngine != null)
{
mFREngine.AFR_FSDK_UninitialEngine();
}
}
/**
* 保存版本信息及特征级别到数据存储路径下的face.txt
*
* @return :返回值为是否保存成功
*/
private boolean saveInfo()
{
try
{
FileOutputStream fs = new FileOutputStream(mDBPath + "/face.txt");
byte[] data = (mFRVersion.toString() + "," + mFRVersion.getFeatureLevel() + " ").getBytes();
fs.write(data);
fs.close();
Log.d(TAG, "保存在face.txt中的信息为:" + mFRVersion.toString() + "," + mFRVersion.getFeatureLevel());
return true;
} catch (FileNotFoundException e)
{
e.printStackTrace();
} catch (IOException e)
{
e.printStackTrace();
}
return false;
}
/**
* 从face.txt中加载图片名称
*
* @return :返回值为是否加载成功
*/
private boolean loadInfo()
{
if (!mRegister.isEmpty())
{//如果不为空
return false;
}
try
{
FileInputStream fs = new FileInputStream(mDBPath + "/face.txt");
byte[] buf = new byte[1024];
int length = 0;
String version_saved = null;
while ((length = fs.read(buf)) != -1)
{
version_saved = new String(buf, 0, length);
}
System.out.println("读取的数据为:" + version_saved);
if (version_saved.equals(mFRVersion.toString() + "," + mFRVersion.getFeatureLevel() + " "))
{
mUpgrade = true;
}
//加载所有已经注册在face.txt中的名称.
if (version_saved != null)
{
String[] names = version_saved.split(" ");
for (int i = 1; i < names.length; i++) //从第二个开始读取
{
if (new File(mDBPath + "/" + names[i] + ".data").exists())
{
//这里拿到的是所有的注册图片的名称
mRegister.add(new FaceRegist(new String(names[i])));
}
}
}
fs.close();
return true;
} catch (FileNotFoundException e)
{
e.printStackTrace();
} catch (IOException e)
{
e.printStackTrace();
}
return false;
}
/**
* 删除文件夹下的文件
*/
public static void delFiles(String filePath)
{
File file = new File(filePath);
if (!file.exists())
{
return;
}
String[] list = file.list();
File temp = null;
String path = null;
for (String item : list)
{
path = filePath + File.separator + item;
temp = new File(path);
if (temp.isFile())
{
temp.delete();
continue;
}
if (temp.isDirectory())
{
delFiles(path);
new File(path).delete();
continue;
}
}
}
/**
* 加载人脸特征数据。在PermissionAcitivity的130行调用此方法
*
* @return :返回值为是否加载成功
*/
public boolean loadFaces()
{
if (loadInfo())
{
try
{
for (FaceRegist face : mRegister)
{
Log.d(TAG, "加载名称为:" + face.mName + "的图片的特征数据");
FileInputStream fs = new FileInputStream(mDBPath + "/" + face.mName + ".data");
ExtInputStream bos = new ExtInputStream(fs);
/**这个类用来保存人脸特征信息*/
AFR_FSDKFace afr = null;
do
{ //第一次先从上到下走循环体,再判断while条件
if (afr != null)
{
if (mUpgrade)
{
//upgrade data.
}
//keyFile为/storage/emulated/0/Android/data/com.arcsoft.sdk_demo/cache/3886197395912.jpg
//读取存入xx.data中的图片路径值
String keyFile = bos.readString();
face.mFaceList.put(keyFile, afr);
}
afr = new AFR_FSDKFace();
} while (bos.readBytes(afr.getFeatureData())); //经过这个条件判断,就把.data中的数据读取到了AFR_FSDKFace中
bos.close();
fs.close();
Log.d(TAG, "加载到的图片数据有:" + face.mFaceList.size() + "条");
}
return true;
} catch (FileNotFoundException e)
{
e.printStackTrace();
} catch (IOException e)
{
e.printStackTrace();
}
}
return false;
}
/**
* 添加人脸特征数据
*
* @param name : 图片名称
* @param face :人脸特征信息
* @param faceicon : Bitmap图片
*/
public void addFace(String name, AFR_FSDKFace face, Bitmap faceicon)
{
try
{
//1、保存人脸图片
String keyPath = mDBPath + "/" + System.nanoTime() + ".jpg";//设置图片链接。System.nanoTime():返回的是纳秒
File keyFile = new File(keyPath);
OutputStream stream = new FileOutputStream(keyFile);
/**compress方法:第一个参数是压缩格式,第二个参数是压缩质量,第三个参数是把压缩图片存到输出流指定的路径去*/
if (faceicon.compress(Bitmap.CompressFormat.JPEG, 80, stream))
{//调用compress方法的时候,其实已经执行了输出流保存的操作
Log.d(TAG, "图片保存成功!");
}
stream.close();
//2、检查是否已经注册
boolean add = true; //默认未注册
for (FaceRegist frface : mRegister)
{
if (frface.mName.equals(name))
{
frface.mFaceList.put(keyPath, face);
add = false; //设置为已注册
break;
}
}
if (add)
{ //如果没有注册
FaceRegist frface = new FaceRegist(name);
frface.mFaceList.put(keyPath, face);
mRegister.add(frface);
}
//3、保存新的图片名称和特征数据
if (saveInfo())
{
//在face.txt中继续添加图片名称
FileOutputStream fs = new FileOutputStream(mDBPath + "/face.txt", true);//true表示追加
for (FaceRegist frface : mRegister)
{
fs.write((frface.mName + " ").getBytes());
}
fs.close();
//追加成功后上传face.txt文件
faceFileUtils.uploadFile("face.txt");
//保存新的人脸特征数据
fs = new FileOutputStream(mDBPath + "/" + name + ".data", true);//true表示追加
ExtOutputStream bos = new ExtOutputStream(fs);
bos.writeBytes(face.getFeatureData());//保存人脸特征数据
bos.writeString(keyPath);//保存图片路径值到xx.data中
bos.close();
fs.close();
//上传.data文件
faceFileUtils.uploadFile(name + ".data");
//等到上传完成再删除
SystemClock.sleep(500);
//上传本目录下的所有文件。注意当前打开程序注册的这一次数据已经加载到集合mRegister中,即使删除所有face.txt文件和xxx.data文件,也不会影响当前注册这一次
delFiles(mDBPath);
}
} catch (FileNotFoundException e)
{
e.printStackTrace();
} catch (IOException e)
{
e.printStackTrace();
}
}
/**
* 删除人脸数据
*
* @param name :图片名称
* @return :是否删除成功
*/
public boolean delete(String name)
{
try
{
//检查是否已经注册
boolean find = false;//删除成功设置为true
for (FaceRegist frface : mRegister)
{
if (frface.mName.equals(name))
{
File delfile = new File(mDBPath + "/" + name + ".data");
if (delfile.exists())
{
delfile.delete();
}
mRegister.remove(frface);
find = true;//删除成功设置为true
break;
}
}
if (find)
{//如果删除成功
if (saveInfo())
{
//更新face.txt中的数据
FileOutputStream fs = new FileOutputStream(mDBPath + "/face.txt", true);//true表示追加
for (FaceRegist frface : mRegister)
{
fs.write((frface.mName + " ").getBytes());
}
fs.close();
}
}
return find;
} catch (FileNotFoundException e)
{
e.printStackTrace();
} catch (IOException e)
{
e.printStackTrace();
}
return false;
}
public boolean upgrade()//未用
{
return false;
}
}
其中使用的三个基于OkHttp的文件上传下载工具类如下:
/***
*脸部数据上传下载工具类
*/
public class FaceFileUtils
{
private String mDBPath;
private String uploadUrl;
private String downloadUrl;
public FaceFileUtils(String mDBPath, String uploadUrl, String downloadUrl)
{
this.mDBPath = mDBPath;
this.uploadUrl = uploadUrl;
this.downloadUrl = downloadUrl;
}
/***
*上传文件到服务器
*
* @param name :上传文件名,包括后缀名
*/
public void uploadFile(String name)
{
//以流的形式上传文件
MediaType type = MediaType.parse("application/octet-stream");
//指向文件
File file = new File(mDBPath + "/" + name);
//创建请求体
RequestBody fileBody = RequestBody.create(type, file);
//构建请求体和请求参数。file是后台接口请求参数名
RequestBody requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM).addFormDataPart("file", name, fileBody).build();
/***
* 上传文件
*/
UploadFile.sendOkHttpRequestPost(uploadUrl, requestBody, new Callback()
{
@Override
public void onFailure(Call call, IOException e)
{
toastShow("数据上传失败!");
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException
{
toastShow("数据上传成功!");
}
});
}
/***
*从服务器下载脸部数据文件
*/
public void downloadFile()
{
//先下载face.txt
download("face.txt", mDBPath);
/**
* 再获取face.txt文件的内容
*/
UploadFile.sendOkHttpRequestGET(downloadUrl + "face.txt", new Callback()
{
@Override
public void onFailure(Call call, IOException e)
{
toastShow("脸部数据获取失败!");
}
@Override
public void onResponse(Call call, Response response) throws IOException
{
//这里能拿到face.txt中的文本数据:内容为:1.2.0.42,0 qwer 123
final String responsedata = response.body().string();
//按照空格切开
String[] names = responsedata.split(" ");
for (int i = 1; i < names.length; i++) //从第二个开始
{
download(names[i] + ".data", mDBPath);
}
}
});
}
/**
* 下载文件
*
* @param fileName :文件名,包括后缀
* @param savePath : 下载文件保存路径
*/
private void download(String fileName, String savePath)
{
DownloadFile.get().download(downloadUrl + fileName, savePath, new DownloadFile.OnDownloadListener()
{
@Override
public void onDownloadSuccess()
{
toastShow("数据同步成功!");
}
@Override
public void onDownloading(int progress)
{
toastShow("进度:" + progress);
}
@Override
public void onDownloadFailed()
{
toastShow("数据同步失败!");
}
});
}
public void toastShow(final String msg)
{
Log.d("文件日志", msg);
}
}
/***
*下载文件工具类
*/
public class DownloadFile
{
private static DownloadFile downloadFile;
private final OkHttpClient okHttpClient;
/***
* 单例模式获取本类对象
* @return
*/
public static DownloadFile get()
{
if (downloadFile == null)
{
downloadFile = new DownloadFile();
}
return downloadFile;
}
private DownloadFile()
{
okHttpClient = new OkHttpClient();
}
/**
* 下载文件
*
* @param url 下载链接
* @param saveDir 储存下载文件的SDCard目录
* @param listener 下载监听
*/
public void download(final String url, final String saveDir, final OnDownloadListener listener)
{
Request request = new Request.Builder().url(url).build();
okHttpClient.newCall(request).enqueue(new Callback()
{
@Override
public void onFailure(okhttp3.Call call, IOException e)
{
// 下载失败
listener.onDownloadFailed();
}
@Override
public void onResponse(okhttp3.Call call, Response response) throws IOException
{
InputStream is = null;
byte[] buf = new byte[2048];
int len = 0;
FileOutputStream fos = null;
try
{
is = response.body().byteStream();
long total = response.body().contentLength();
File file = new File(saveDir, getNameFromUrl(url));
fos = new FileOutputStream(file);
long sum = 0;
while ((len = is.read(buf)) != -1)
{
fos.write(buf, 0, len);
sum += len;
int progress = (int) (sum * 1.0f / total * 100);
// 下载中
listener.onDownloading(progress);
}
fos.flush();
// 下载完成
listener.onDownloadSuccess();
} catch (Exception e)
{
listener.onDownloadFailed();
} finally
{
try
{
if (is != null) is.close();
} catch (IOException e)
{
}
try
{
if (fos != null) fos.close();
} catch (IOException e)
{
}
}
}
});
}
/**
* 从下载连接中解析出文件名。以=为界
*
* @param url :下载连接
* @return 文件名
*/
private String getNameFromUrl(String url)
{
return url.substring(url.lastIndexOf("=") + 1);
}
public interface OnDownloadListener
{
/**
* 下载成功
*/
void onDownloadSuccess();
/**
* 下载进度
*
* @param progress :进度值
*/
void onDownloading(int progress);
/**
* 下载失败
*/
void onDownloadFailed();
}
}
/***
*上传文件工具类
*/
public class UploadFile
{
/**
* POST方式访问网络
*
* @param address :请求地址
* @param requestBody :请求体
* @param callback :请求回调
*/
public static void sendOkHttpRequestPost(String address, RequestBody requestBody, okhttp3.Callback callback)
{
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(address).post(requestBody).build();
client.newCall(request).enqueue(callback);
}
/**
* GET方式访问网络
*
* @param address :请求地址
* @param callback :请求回调
*/
public static void sendOkHttpRequestGET(String address, okhttp3.Callback callback)
{
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(address).build();
client.newCall(request).enqueue(callback);
}
}
Android完整代码链接:
四、java版本的文件上传下载后台如下:
创建SpringBoot项目,pom.xml文件如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
application.yml如下:
server:
port: 10086
spring:
application:
name: aboutfile
servlet:
multipart:
enabled: true #启用文件上传,默认为true
max-file-size: 10MB #单个文件最大值(KB/MB)
max-request-size: 10MB #单个请求多个文件的最大值(KB/MB)
file-size-threshold: 0KB #文件大于该阈值时,将写入磁盘(KB/MB)
主文件如下:
@SpringBootApplication
public class FileUploadForJavaApp
{
public static void main(String[] args)
{
SpringApplication.run(FileUploadForJavaApp.class, args);
}
}
@Controller
public class UploadController
{
@Autowired
private FileManageService fileManageService;
@PostMapping("/upload")
@ResponseBody
public String fileUpload(@RequestParam("file") MultipartFile file) throws Exception
{
if (Objects.isNull(file))
{
return "文件上传失败,请重新选择文件";
}
return fileManageService.upload(file);
}
}
@Controller
public class DownLoadController
{
@Autowired
private FileManageService fileManageService;
@GetMapping("/download")
@ResponseBody
public String fileDownload(@RequestParam("fileName") String fileName, HttpServletResponse response)
{
if (Objects.isNull(fileName))
{
return "文件下载失败,请选择文件要下载的文件";
}
return fileManageService.download(fileName, response);
}
}
@Service
public class FileManageService
{
private static final Logger LOGGER = LoggerFactory.getLogger(FileManageService.class);
private String fileHostPath = "C:\\test\\";
/**
* 上传文件
*
* @param file
* @return
*/
public String upload(MultipartFile file) throws Exception
{
String fileName = file.getOriginalFilename(); //获取文件名
File test = new File(fileHostPath + fileName);
String[] ext = fileName.split("\\.");//获取后缀名。如果是txt,就要设置为追加。否则直接上传
if (ext[1].equals("txt") && test.exists())
{//如果是以txt结尾的face.txt文件,如果存在就追加
InputStream fis = file.getInputStream();
String content = readInputStream(fis);
String[] split = content.split(" ");
fis = getStringStream(split[split.length - 1] + " "); //只取最后一个
File fileExist = new File(fileHostPath + fileName);
OutputStream fos = new FileOutputStream(fileExist, true);
byte[] flush = new byte[1024];
int len = -1;
while ((len = fis.read(flush)) != -1)
{
fos.write(flush, 0, len);
}
fis.close();
fos.close();
} else
{
test.createNewFile();
file.transferTo(test);
}
return "文件上传成功";
}
/**
* 把输入流的内容转化成字符串
*
* @param is
* @return 字符串
*/
public static String readInputStream(InputStream is)
{
try
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int length = 0;
byte[] buffer = new byte[1024];
while ((length = is.read(buffer)) != -1)
{
baos.write(buffer, 0, length);
}
is.close();
baos.close();
//或者用这种方法
//byte[] result=baos.toByteArray();
//return new String(result);
return baos.toString();
} catch (Exception e)
{
e.printStackTrace();
return "获取失败";
}
}
/**
* 把字符串转为输入流
*
* @param sInputString
* @return
*/
public static InputStream getStringStream(String sInputString)
{
if (sInputString != null && !sInputString.trim().equals(""))
{
try
{
ByteArrayInputStream tInputStringStream = new ByteArrayInputStream(sInputString.getBytes());
return tInputStringStream;
} catch (Exception ex)
{
ex.printStackTrace();
}
}
return null;
}
/****
* 下载文件
* @param fileName
* @param response
* @return
*/
public String download(String fileName, HttpServletResponse response)
{
File file = new File(fileHostPath + fileName);
if (!file.exists())
{
return "文件不存在";
}
byte[] bytes = new byte[1024];
BufferedInputStream bufferedInputStream = null;
OutputStream outputStream = null;
FileInputStream fileInputStream = null;
try
{
fileInputStream = new FileInputStream(file);
bufferedInputStream = new BufferedInputStream(fileInputStream);
response.setContentType(MediaType.APPLICATION_OCTET_STREAM.toString());
response.addHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(fileName, "UTF-8"));
outputStream = response.getOutputStream();
int length;
while ((length = bufferedInputStream.read(bytes)) != -1)
{
outputStream.write(bytes, 0, length);
}
outputStream.flush();
} catch (Exception e)
{
LOGGER.error("文件下载失败", e);
} finally
{
try
{
if (bufferedInputStream != null)
{
bufferedInputStream.close();
}
if (outputStream != null)
{
outputStream.close();
}
if (fileInputStream != null)
{
fileInputStream.close();
}
} catch (IOException e)
{
LOGGER.error(e.getMessage(), e);
}
}
return "文件下载成功!";
}
}
完整项目下载链接: