|
|
package com.yhkj.rebotsdk.engine.arcface
|
|
|
|
|
|
import android.content.Context
|
|
|
import android.graphics.Bitmap
|
|
|
import android.graphics.Rect
|
|
|
import com.apkfuns.logutils.LogUtils
|
|
|
import com.arcsoft.face.ErrorInfo
|
|
|
import com.arcsoft.face.FaceEngine
|
|
|
import com.arcsoft.face.FaceFeature
|
|
|
import com.arcsoft.face.FaceInfo
|
|
|
import com.arcsoft.face.enums.DetectFaceOrientPriority
|
|
|
import com.arcsoft.face.enums.DetectMode
|
|
|
import com.arcsoft.imageutil.ArcSoftImageFormat
|
|
|
import com.arcsoft.imageutil.ArcSoftImageUtil
|
|
|
import com.arcsoft.imageutil.ArcSoftImageUtilError
|
|
|
import com.arcsoft.imageutil.ArcSoftRotateDegree
|
|
|
import com.yhkj.rebotsdk.engine.EngineInterface
|
|
|
import com.yhkj.rebotsdk.engine.EngineStatusCode
|
|
|
import com.yhkj.rebotsdk.engine.EngineStatusData
|
|
|
import com.yhkj.rebotsdk.engine.IdentifyData
|
|
|
import java.io.File
|
|
|
import java.io.FileInputStream
|
|
|
import java.io.FileOutputStream
|
|
|
import java.io.IOException
|
|
|
import kotlin.math.max
|
|
|
import kotlin.math.min
|
|
|
|
|
|
/**
|
|
|
* Created by zhangweiwei on 2020/3/16.
|
|
|
*/
|
|
|
internal class ArcFaceEngine : EngineInterface {
|
|
|
|
|
|
|
|
|
private var faceEngine: FaceEngine? = null
|
|
|
private var faceRegisterInfoList: ArrayList<FaceRegisterInfo>? = null
|
|
|
private var ROOT_PATH: String? = null
|
|
|
private val IMG_SUFFIX = ".jpg"
|
|
|
|
|
|
/**
|
|
|
* 存放注册图的目录
|
|
|
*/
|
|
|
private val SAVE_IMG_DIR = "register" + File.separator + "imgs"
|
|
|
|
|
|
/**
|
|
|
* 存放特征的目录
|
|
|
*/
|
|
|
private val SAVE_FEATURE_DIR = "register" + File.separator + "features"
|
|
|
|
|
|
|
|
|
override fun activeFace(
|
|
|
context: Context,
|
|
|
active_key: String,
|
|
|
appId: String,
|
|
|
sdkKey: String
|
|
|
): Int {
|
|
|
return FaceEngine.activeOnline(
|
|
|
context, active_key, appId, sdkKey
|
|
|
)
|
|
|
}
|
|
|
|
|
|
override fun init(context: Context?): EngineStatusData {
|
|
|
synchronized(this) {
|
|
|
if (faceEngine == null && context != null) {
|
|
|
faceEngine = FaceEngine()
|
|
|
val engineCode: Int = faceEngine!!.init(
|
|
|
context,
|
|
|
DetectMode.ASF_DETECT_MODE_IMAGE,
|
|
|
DetectFaceOrientPriority.ASF_OP_0_ONLY,
|
|
|
16,
|
|
|
1,
|
|
|
FaceEngine.ASF_FACE_RECOGNITION or FaceEngine.ASF_FACE_DETECT
|
|
|
)
|
|
|
return if (engineCode == ErrorInfo.MOK) {
|
|
|
initFaceList(context)
|
|
|
EngineStatusData(
|
|
|
EngineStatusCode.SUCCEED,
|
|
|
"init succeed"
|
|
|
)
|
|
|
} else {
|
|
|
faceEngine = null
|
|
|
EngineStatusData(
|
|
|
EngineStatusCode.FAILED,
|
|
|
"init failed"
|
|
|
)
|
|
|
}
|
|
|
}
|
|
|
return EngineStatusData(
|
|
|
EngineStatusCode.FAILED,
|
|
|
"init failed"
|
|
|
)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
override fun unInit() {
|
|
|
synchronized(this) {
|
|
|
faceRegisterInfoList?.clear()
|
|
|
faceRegisterInfoList = null
|
|
|
faceEngine?.unInit()
|
|
|
faceEngine = null
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 初始化人脸特征数据以及人脸特征数据对应的注册图
|
|
|
*
|
|
|
* @param context 上下文对象
|
|
|
*/
|
|
|
private fun initFaceList(context: Context) {
|
|
|
synchronized(this) {
|
|
|
if (ROOT_PATH == null) {
|
|
|
ROOT_PATH = context.filesDir.absolutePath
|
|
|
}
|
|
|
val featureDir =
|
|
|
File(ROOT_PATH + File.separator + SAVE_FEATURE_DIR)
|
|
|
if (!featureDir.exists() || !featureDir.isDirectory) {
|
|
|
return
|
|
|
}
|
|
|
val featureFiles: Array<File> = featureDir.listFiles()
|
|
|
if (featureFiles.isEmpty()) {
|
|
|
return
|
|
|
}
|
|
|
faceRegisterInfoList = java.util.ArrayList<FaceRegisterInfo>()
|
|
|
for (featureFile in featureFiles) {
|
|
|
try {
|
|
|
val fis = FileInputStream(featureFile)
|
|
|
val feature = ByteArray(FaceFeature.FEATURE_SIZE)
|
|
|
fis.read(feature)
|
|
|
fis.close()
|
|
|
faceRegisterInfoList!!.add(
|
|
|
FaceRegisterInfo(feature, featureFile.name)
|
|
|
)
|
|
|
} catch (e: IOException) {
|
|
|
e.printStackTrace()
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
override fun getFaceNumber(context: Context?): Int {
|
|
|
synchronized(this) {
|
|
|
if (context == null) {
|
|
|
return 0
|
|
|
}
|
|
|
if (ROOT_PATH == null) {
|
|
|
ROOT_PATH = context.filesDir.absolutePath
|
|
|
}
|
|
|
val featureFileDir =
|
|
|
File(ROOT_PATH + File.separator + SAVE_FEATURE_DIR)
|
|
|
var featureCount = 0
|
|
|
if (featureFileDir.exists() && featureFileDir.isDirectory) {
|
|
|
val featureFiles: Array<String> = featureFileDir.list()
|
|
|
featureCount = featureFiles.size
|
|
|
}
|
|
|
var imageCount = 0
|
|
|
val imgFileDir =
|
|
|
File(ROOT_PATH + File.separator + SAVE_IMG_DIR)
|
|
|
if (imgFileDir.exists() && imgFileDir.isDirectory) {
|
|
|
val imageFiles: Array<String> = imgFileDir.list()
|
|
|
imageCount = imageFiles.size
|
|
|
}
|
|
|
return if (featureCount > imageCount) imageCount else featureCount
|
|
|
}
|
|
|
}
|
|
|
|
|
|
override fun clearAllFaces(context: Context?): Int {
|
|
|
synchronized(this) {
|
|
|
if (context == null) {
|
|
|
return 0
|
|
|
}
|
|
|
if (ROOT_PATH == null) {
|
|
|
ROOT_PATH = context.filesDir.absolutePath
|
|
|
}
|
|
|
faceRegisterInfoList?.clear()
|
|
|
val featureFileDir =
|
|
|
File(ROOT_PATH + File.separator + SAVE_FEATURE_DIR)
|
|
|
var deletedFeatureCount = 0
|
|
|
if (featureFileDir.exists() && featureFileDir.isDirectory) {
|
|
|
val featureFiles: Array<File> = featureFileDir.listFiles()
|
|
|
if (featureFiles.isNotEmpty()) {
|
|
|
for (featureFile in featureFiles) {
|
|
|
if (featureFile.delete()) {
|
|
|
deletedFeatureCount++
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
var deletedImageCount = 0
|
|
|
val imgFileDir =
|
|
|
File(ROOT_PATH + File.separator + SAVE_IMG_DIR)
|
|
|
if (imgFileDir.exists() && imgFileDir.isDirectory) {
|
|
|
val imgFiles: Array<File> = imgFileDir.listFiles()
|
|
|
if (imgFiles.isNotEmpty()) {
|
|
|
for (imgFile in imgFiles) {
|
|
|
if (imgFile.delete()) {
|
|
|
deletedImageCount++
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return if (deletedFeatureCount > deletedImageCount) deletedImageCount else deletedFeatureCount
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
override fun registerNv21(
|
|
|
context: Context?,
|
|
|
nv21: ByteArray?,
|
|
|
width: Int,
|
|
|
height: Int,
|
|
|
faceId: String?
|
|
|
): Boolean {
|
|
|
synchronized(this) {
|
|
|
if (faceEngine == null || context == null || nv21 == null || width % 4 != 0 || nv21.size != width * height * 3 / 2) {
|
|
|
LogUtils.e("registerNv21: invalid params")
|
|
|
return false
|
|
|
}
|
|
|
val faceInfoList: List<FaceInfo>? = null
|
|
|
val faceInfoCode: Int = faceEngine!!.detectFaces(
|
|
|
nv21,
|
|
|
width,
|
|
|
height,
|
|
|
FaceEngine.CP_PAF_NV21,
|
|
|
faceInfoList
|
|
|
)
|
|
|
if(faceInfoCode != ErrorInfo.MOK){
|
|
|
return false
|
|
|
}
|
|
|
TrackUtil.keepMaxFace(faceInfoList)
|
|
|
val faceInfo=faceInfoList?.get(0)
|
|
|
if (ROOT_PATH == null) {
|
|
|
ROOT_PATH = context.filesDir.absolutePath
|
|
|
}
|
|
|
//特征存储的文件夹
|
|
|
val featureDir =
|
|
|
File(ROOT_PATH + File.separator + SAVE_FEATURE_DIR)
|
|
|
if (!featureDir.exists() && !featureDir.mkdirs()) {
|
|
|
LogUtils.e("registerNv21: can not create feature directory")
|
|
|
return false
|
|
|
}
|
|
|
//图片存储的文件夹
|
|
|
val imgDir =
|
|
|
File(ROOT_PATH + File.separator + SAVE_IMG_DIR)
|
|
|
if (!imgDir.exists() && !imgDir.mkdirs()) {
|
|
|
LogUtils.e("registerNv21: can not create image directory")
|
|
|
return false
|
|
|
}
|
|
|
val faceFeature = FaceFeature()
|
|
|
//特征提取
|
|
|
val code: Int =
|
|
|
faceEngine!!.extractFaceFeature(
|
|
|
nv21,
|
|
|
width,
|
|
|
height,
|
|
|
FaceEngine.CP_PAF_NV21,
|
|
|
faceInfo,
|
|
|
faceFeature
|
|
|
)
|
|
|
return if (code != ErrorInfo.MOK) {
|
|
|
LogUtils.e("registerNv21: extractFaceFeature failed , code is $code")
|
|
|
false
|
|
|
} else {
|
|
|
val registerFaceId =
|
|
|
faceId ?: System.currentTimeMillis().toString()
|
|
|
try {
|
|
|
// 保存注册结果(注册图、特征数据)
|
|
|
// 为了美观,扩大rect截取注册图
|
|
|
val cropRect: Rect? =
|
|
|
getBestRect(width, height, faceInfo!!.rect)
|
|
|
if (cropRect == null) {
|
|
|
LogUtils.e("registerNv21: cropRect is null!")
|
|
|
return false
|
|
|
}
|
|
|
cropRect.left = cropRect.left and 3.inv()
|
|
|
cropRect.top = cropRect.top and 3.inv()
|
|
|
cropRect.right = cropRect.right and 3.inv()
|
|
|
cropRect.bottom = cropRect.bottom and 3.inv()
|
|
|
val file =
|
|
|
File(imgDir.toString() + File.separator + registerFaceId + IMG_SUFFIX)
|
|
|
|
|
|
|
|
|
// 创建一个头像的Bitmap,存放旋转结果图
|
|
|
val headBmp: Bitmap? = getHeadImage(
|
|
|
nv21,
|
|
|
width,
|
|
|
height,
|
|
|
faceInfo.orient,
|
|
|
cropRect,
|
|
|
ArcSoftImageFormat.NV21
|
|
|
)
|
|
|
val fosImage = FileOutputStream(file)
|
|
|
headBmp?.compress(Bitmap.CompressFormat.JPEG, 100, fosImage)
|
|
|
fosImage.close()
|
|
|
val fosFeature =
|
|
|
FileOutputStream(featureDir.toString() + File.separator + registerFaceId)
|
|
|
fosFeature.write(faceFeature.featureData)
|
|
|
fosFeature.close()
|
|
|
|
|
|
//内存中的数据同步
|
|
|
if (faceRegisterInfoList == null) {
|
|
|
faceRegisterInfoList =
|
|
|
java.util.ArrayList<FaceRegisterInfo>()
|
|
|
}
|
|
|
faceRegisterInfoList!!.add(
|
|
|
FaceRegisterInfo(faceFeature.featureData, registerFaceId)
|
|
|
)
|
|
|
true
|
|
|
} catch (e: IOException) {
|
|
|
e.printStackTrace()
|
|
|
false
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
override fun registerBgr24(
|
|
|
context: Context?,
|
|
|
bgr24: ByteArray?,
|
|
|
width: Int,
|
|
|
height: Int,
|
|
|
faceId: String?
|
|
|
): Boolean {
|
|
|
synchronized(this) {
|
|
|
if (faceEngine == null || context == null || bgr24 == null || width % 4 != 0 || bgr24.size != width * height * 3) {
|
|
|
LogUtils.e("registerBgr24: invalid params")
|
|
|
return false
|
|
|
}
|
|
|
if (ROOT_PATH == null) {
|
|
|
ROOT_PATH = context.filesDir.absolutePath
|
|
|
}
|
|
|
//特征存储的文件夹
|
|
|
val featureDir =
|
|
|
File(ROOT_PATH + File.separator + SAVE_FEATURE_DIR)
|
|
|
if (!featureDir.exists() && !featureDir.mkdirs()) {
|
|
|
LogUtils.e("registerBgr24: can not create feature directory")
|
|
|
return false
|
|
|
}
|
|
|
//图片存储的文件夹
|
|
|
val imgDir =
|
|
|
File(ROOT_PATH + File.separator + SAVE_IMG_DIR)
|
|
|
if (!imgDir.exists() && !imgDir.mkdirs()) {
|
|
|
LogUtils.e("registerBgr24: can not create image directory")
|
|
|
return false
|
|
|
}
|
|
|
//人脸检测
|
|
|
val faceInfoList: List<FaceInfo> = java.util.ArrayList()
|
|
|
var code: Int = faceEngine!!.detectFaces(
|
|
|
bgr24,
|
|
|
width,
|
|
|
height,
|
|
|
FaceEngine.CP_PAF_BGR24,
|
|
|
faceInfoList
|
|
|
)
|
|
|
return if (code == ErrorInfo.MOK && faceInfoList.size > 0) {
|
|
|
val faceFeature = FaceFeature()
|
|
|
|
|
|
//特征提取
|
|
|
code = faceEngine!!.extractFaceFeature(
|
|
|
bgr24,
|
|
|
width,
|
|
|
height,
|
|
|
FaceEngine.CP_PAF_BGR24,
|
|
|
faceInfoList[0],
|
|
|
faceFeature
|
|
|
)
|
|
|
val registerFaceId =
|
|
|
faceId ?: System.currentTimeMillis().toString()
|
|
|
try {
|
|
|
//保存注册结果(注册图、特征数据)
|
|
|
if (code == ErrorInfo.MOK) {
|
|
|
//为了美观,扩大rect截取注册图
|
|
|
val cropRect: Rect? =
|
|
|
getBestRect(
|
|
|
width,
|
|
|
height,
|
|
|
faceInfoList[0].rect
|
|
|
)
|
|
|
if (cropRect == null) {
|
|
|
LogUtils.e("registerBgr24: cropRect is null")
|
|
|
return false
|
|
|
}
|
|
|
cropRect.left = cropRect.left and 3.inv()
|
|
|
cropRect.top = cropRect.top and 3.inv()
|
|
|
cropRect.right = cropRect.right and 3.inv()
|
|
|
cropRect.bottom = cropRect.bottom and 3.inv()
|
|
|
val file =
|
|
|
File(imgDir.toString() + File.separator + registerFaceId + IMG_SUFFIX)
|
|
|
val fosImage = FileOutputStream(file)
|
|
|
|
|
|
|
|
|
// 创建一个头像的Bitmap,存放旋转结果图
|
|
|
val headBmp = getHeadImage(
|
|
|
bgr24,
|
|
|
width,
|
|
|
height,
|
|
|
faceInfoList[0].orient,
|
|
|
cropRect,
|
|
|
ArcSoftImageFormat.BGR24
|
|
|
)
|
|
|
// 保存到本地
|
|
|
headBmp!!.compress(Bitmap.CompressFormat.JPEG, 100, fosImage)
|
|
|
fosImage.close()
|
|
|
|
|
|
// 保存特征数据
|
|
|
val fosFeature =
|
|
|
FileOutputStream(featureDir.toString() + File.separator + registerFaceId)
|
|
|
fosFeature.write(faceFeature.featureData)
|
|
|
fosFeature.close()
|
|
|
|
|
|
// 内存中的数据同步
|
|
|
if (faceRegisterInfoList == null) {
|
|
|
faceRegisterInfoList =
|
|
|
java.util.ArrayList<FaceRegisterInfo>()
|
|
|
}
|
|
|
faceRegisterInfoList?.apply {
|
|
|
this.add(
|
|
|
FaceRegisterInfo(faceFeature.featureData, registerFaceId)
|
|
|
)
|
|
|
}
|
|
|
|
|
|
true
|
|
|
} else {
|
|
|
LogUtils.e("registerBgr24: extract face feature failed, code is $code")
|
|
|
false
|
|
|
}
|
|
|
} catch (e: IOException) {
|
|
|
e.printStackTrace()
|
|
|
false
|
|
|
}
|
|
|
} else {
|
|
|
LogUtils.e("registerBgr24: no face detected, code is $code")
|
|
|
false
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
override fun identify(sourceData: ByteArray): IdentifyData {
|
|
|
TODO("Not yet implemented")
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 截取合适的头像并旋转,保存为注册头像
|
|
|
*
|
|
|
* @param originImageData 原始的BGR24数据
|
|
|
* @param width BGR24图像宽度
|
|
|
* @param height BGR24图像高度
|
|
|
* @param orient 人脸角度
|
|
|
* @param cropRect 裁剪的位置
|
|
|
* @param imageFormat 图像格式
|
|
|
* @return 头像的图像数据
|
|
|
*/
|
|
|
private fun getHeadImage(
|
|
|
originImageData: ByteArray,
|
|
|
width: Int,
|
|
|
height: Int,
|
|
|
orient: Int,
|
|
|
cropRect: Rect,
|
|
|
imageFormat: ArcSoftImageFormat
|
|
|
): Bitmap? {
|
|
|
val headImageData =
|
|
|
ArcSoftImageUtil.createImageData(cropRect.width(), cropRect.height(), imageFormat)
|
|
|
val cropCode = ArcSoftImageUtil.cropImage(
|
|
|
originImageData,
|
|
|
headImageData,
|
|
|
width,
|
|
|
height,
|
|
|
cropRect,
|
|
|
imageFormat
|
|
|
)
|
|
|
if (cropCode != ArcSoftImageUtilError.CODE_SUCCESS) {
|
|
|
throw RuntimeException("crop image failed, code is $cropCode")
|
|
|
}
|
|
|
|
|
|
//判断人脸旋转角度,若不为0度则旋转注册图
|
|
|
var rotateHeadImageData: ByteArray? = null
|
|
|
val rotateCode: Int
|
|
|
val cropImageWidth: Int
|
|
|
val cropImageHeight: Int
|
|
|
// 90度或270度的情况,需要宽高互换
|
|
|
if (orient == FaceEngine.ASF_OC_90 || orient == FaceEngine.ASF_OC_270) {
|
|
|
cropImageWidth = cropRect.height()
|
|
|
cropImageHeight = cropRect.width()
|
|
|
} else {
|
|
|
cropImageWidth = cropRect.width()
|
|
|
cropImageHeight = cropRect.height()
|
|
|
}
|
|
|
var rotateDegree: ArcSoftRotateDegree? = null
|
|
|
when (orient) {
|
|
|
FaceEngine.ASF_OC_90 -> rotateDegree = ArcSoftRotateDegree.DEGREE_270
|
|
|
FaceEngine.ASF_OC_180 -> rotateDegree = ArcSoftRotateDegree.DEGREE_180
|
|
|
FaceEngine.ASF_OC_270 -> rotateDegree = ArcSoftRotateDegree.DEGREE_90
|
|
|
FaceEngine.ASF_OC_0 -> rotateHeadImageData = headImageData
|
|
|
else -> rotateHeadImageData = headImageData
|
|
|
}
|
|
|
// 非0度的情况,旋转图像
|
|
|
if (rotateDegree != null) {
|
|
|
rotateHeadImageData = ByteArray(headImageData.size)
|
|
|
rotateCode = ArcSoftImageUtil.rotateImage(
|
|
|
headImageData,
|
|
|
rotateHeadImageData,
|
|
|
cropRect.width(),
|
|
|
cropRect.height(),
|
|
|
rotateDegree,
|
|
|
imageFormat
|
|
|
)
|
|
|
if (rotateCode != ArcSoftImageUtilError.CODE_SUCCESS) {
|
|
|
throw RuntimeException("rotate image failed, code is $rotateCode")
|
|
|
}
|
|
|
}
|
|
|
// 将创建一个Bitmap,并将图像数据存放到Bitmap中
|
|
|
val headBmp =
|
|
|
Bitmap.createBitmap(cropImageWidth, cropImageHeight, Bitmap.Config.RGB_565)
|
|
|
if (ArcSoftImageUtil.imageDataToBitmap(
|
|
|
rotateHeadImageData,
|
|
|
headBmp,
|
|
|
imageFormat
|
|
|
) != ArcSoftImageUtilError.CODE_SUCCESS
|
|
|
) {
|
|
|
throw RuntimeException("failed to transform image data to bitmap")
|
|
|
}
|
|
|
return headBmp
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 将图像中需要截取的Rect向外扩张一倍,若扩张一倍会溢出,则扩张到边界,若Rect已溢出,则收缩到边界
|
|
|
*
|
|
|
* @param width 图像宽度
|
|
|
* @param height 图像高度
|
|
|
* @param srcRect 原Rect
|
|
|
* @return 调整后的Rect
|
|
|
*/
|
|
|
private fun getBestRect(width: Int, height: Int, srcRect: Rect?): Rect? {
|
|
|
if (srcRect == null) {
|
|
|
return null
|
|
|
}
|
|
|
val rect = Rect(srcRect)
|
|
|
|
|
|
// 原rect边界已溢出宽高的情况
|
|
|
val maxOverFlow = max(
|
|
|
-rect.left,
|
|
|
max(
|
|
|
-rect.top,
|
|
|
max(rect.right - width, rect.bottom - height)
|
|
|
)
|
|
|
)
|
|
|
if (maxOverFlow >= 0) {
|
|
|
rect.inset(maxOverFlow, maxOverFlow)
|
|
|
return rect
|
|
|
}
|
|
|
|
|
|
// 原rect边界未溢出宽高的情况
|
|
|
var padding = rect.height() / 2
|
|
|
|
|
|
// 若以此padding扩张rect会溢出,取最大padding为四个边距的最小值
|
|
|
if (!(rect.left - padding > 0 && rect.right + padding < width && rect.top - padding > 0 && rect.bottom + padding < height)) {
|
|
|
padding = min(
|
|
|
min(
|
|
|
min(
|
|
|
rect.left,
|
|
|
width - rect.right
|
|
|
), height - rect.bottom
|
|
|
), rect.top
|
|
|
)
|
|
|
}
|
|
|
rect.inset(-padding, -padding)
|
|
|
return rect
|
|
|
}
|
|
|
|
|
|
|
|
|
} |
|
|
\ No newline at end of file |
...
|
...
|
|