|
|
@@ -1,480 +0,0 @@
|
|
|
-package com.grkj.shared.utils.face.arcsoft
|
|
|
-
|
|
|
-import android.content.Context
|
|
|
-import android.graphics.Bitmap
|
|
|
-import android.graphics.ImageFormat
|
|
|
-import android.graphics.Rect
|
|
|
-import android.hardware.camera2.CameraCharacteristics
|
|
|
-import android.hardware.camera2.CameraManager
|
|
|
-import android.media.Image
|
|
|
-import android.media.ImageReader
|
|
|
-import android.os.Environment
|
|
|
-import android.util.Log
|
|
|
-import android.util.Size
|
|
|
-import com.arcsoft.face.*
|
|
|
-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 java.io.File
|
|
|
-import java.io.FileInputStream
|
|
|
-import java.io.FileOutputStream
|
|
|
-import java.io.IOException
|
|
|
-import kotlin.math.abs
|
|
|
-
|
|
|
-object FaceUtils {
|
|
|
- val EXT_ROOT_PATH : String = Environment.getExternalStorageDirectory().absolutePath
|
|
|
- private var faceEngine: FaceEngine? = null
|
|
|
-// private val faceDataList = ArrayList<FaceBean>()
|
|
|
-
|
|
|
- /**
|
|
|
- * 存放特征的目录
|
|
|
- */
|
|
|
- private const val NEW_FEATURE_DIR = "face-features"
|
|
|
-
|
|
|
- fun extFile(path: String) : File {
|
|
|
- return File(EXT_ROOT_PATH + File.separator + "refuse-class" + File.separator + path)
|
|
|
- }
|
|
|
-
|
|
|
- fun moveOldFolderToNew() {
|
|
|
- val oldRegDir = File(EXT_ROOT_PATH + File.separator + "register")
|
|
|
- if (!oldRegDir.exists() || !oldRegDir.isDirectory) {
|
|
|
- Log.i("FACE", "旧目录 register 不存在")
|
|
|
- return
|
|
|
- }
|
|
|
- val oldDir = File(oldRegDir.path + File.separator + "features")
|
|
|
- if (!oldDir.exists() || !oldDir.isDirectory) {
|
|
|
- Log.i("FACE", "旧目录 register/features 不存在")
|
|
|
- return
|
|
|
- }
|
|
|
- val newDir = extFile(NEW_FEATURE_DIR)
|
|
|
- if (!newDir.exists()) {
|
|
|
- val success = newDir.mkdirs()
|
|
|
- Log.i("FACE", "创建新目录 /refuse-class/face-features : ${success}")
|
|
|
- }
|
|
|
- for (oldFile in oldDir.listFiles()) {
|
|
|
- val newFile = extFile(NEW_FEATURE_DIR + File.separator + oldFile.name)
|
|
|
- if (!newFile.exists()) {
|
|
|
- newFile.createNewFile()
|
|
|
- }
|
|
|
- val data = ByteArray(FaceFeature.FEATURE_SIZE)
|
|
|
- val fis = FileInputStream(oldFile)
|
|
|
- fis.read(data)
|
|
|
- fis.close()
|
|
|
- Log.i("FACE", "读取旧目录人脸特征:${oldFile.name}")
|
|
|
- val fos = FileOutputStream(newFile)
|
|
|
- fos.write(data)
|
|
|
- fos.close()
|
|
|
- Log.i("FACE", "写入新目录人脸特征:${oldFile.name}")
|
|
|
- oldFile.delete()
|
|
|
- Log.i("FACE", "删除旧目录人脸特征:${oldFile.name}")
|
|
|
- }
|
|
|
- oldDir.delete()
|
|
|
- Log.i("FACE", "删除旧目录 register/features")
|
|
|
- oldRegDir.delete()
|
|
|
- Log.i("FACE", "删除旧目录 register")
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
- /**
|
|
|
- * 初始化
|
|
|
- *
|
|
|
- * @param context 上下文对象
|
|
|
- * @return 是否初始化成功
|
|
|
- */
|
|
|
- fun init(context: Context): Int {
|
|
|
- synchronized(this) {
|
|
|
- if (faceEngine == null) {
|
|
|
- val t0 = System.currentTimeMillis()
|
|
|
- faceEngine = FaceEngine()
|
|
|
- val engineCode = faceEngine!!.init(
|
|
|
- context,
|
|
|
- DetectMode.ASF_DETECT_MODE_VIDEO,
|
|
|
- DetectFaceOrientPriority.ASF_OP_ALL_OUT,
|
|
|
- 16,
|
|
|
- 1,
|
|
|
- // 人脸监测 | 人脸特征 | RGB 活体
|
|
|
- FaceEngine.ASF_FACE_DETECT or FaceEngine.ASF_FACE_RECOGNITION or FaceEngine.ASF_LIVENESS
|
|
|
- )
|
|
|
- val tt = System.currentTimeMillis() - t0
|
|
|
- Log.i("face", "人脸识别引擎初始化:$engineCode, 耗时:$tt")
|
|
|
- if (engineCode == ErrorInfo.MOK) {
|
|
|
- initFaceList()
|
|
|
- } else {
|
|
|
- faceEngine = null
|
|
|
- }
|
|
|
- return engineCode
|
|
|
- }
|
|
|
- return ErrorInfo.MOK
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 销毁
|
|
|
- */
|
|
|
-// fun unInit() {
|
|
|
-// synchronized(this) {
|
|
|
-// faceDataList.clear()
|
|
|
-// faceEngine?.unInit()
|
|
|
-// faceEngine = null
|
|
|
-// }
|
|
|
-// }
|
|
|
-
|
|
|
- /**
|
|
|
- * 清空人脸特性
|
|
|
- */
|
|
|
- fun clearFaces() {
|
|
|
- extFile(NEW_FEATURE_DIR).delete()
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 初始化人脸特征数据以及人脸特征数据对应的注册图
|
|
|
- *
|
|
|
- */
|
|
|
- private fun initFaceList() {
|
|
|
- synchronized(this) {
|
|
|
- val featureDir = extFile(NEW_FEATURE_DIR)
|
|
|
- if (!featureDir.exists() || !featureDir.isDirectory) {
|
|
|
- return
|
|
|
- }
|
|
|
- for (featureFile in featureDir.listFiles()) {
|
|
|
- try {
|
|
|
- val fis = FileInputStream(featureFile)
|
|
|
- val feature = ByteArray(FaceFeature.FEATURE_SIZE)
|
|
|
- fis.read(feature)
|
|
|
- fis.close()
|
|
|
-// faceDataList.add(FaceBean(featureFile.name, feature))
|
|
|
- } catch (e: IOException) {
|
|
|
- Log.e("face", "加载人脸数据异常", e)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 检测人脸信息
|
|
|
- */
|
|
|
- fun detectFace(nv21: ByteArray, width: Int, height: Int) : FaceInfo? {
|
|
|
- return faceEngine?.run {
|
|
|
- val faceInfos = ArrayList<FaceInfo>()
|
|
|
- val code = detectFaces(nv21, width, height, FaceEngine.CP_PAF_NV21, faceInfos)
|
|
|
- if (code == ErrorInfo.MOK && faceInfos.size > 0) {
|
|
|
- Log.d("face", "检测到 ${faceInfos.size} 个人脸")
|
|
|
- return faceInfos.maxByOrNull { faceInfo ->
|
|
|
- val r = faceInfo.rect
|
|
|
- abs((r.left - r.right) * (r.top - r.bottom))
|
|
|
- }
|
|
|
- } else {
|
|
|
- Log.i("face", "未检测到人脸: $code")
|
|
|
- return null
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 活体检测
|
|
|
- */
|
|
|
- fun detectLive(nv21: ByteArray, faceInfo: FaceInfo, width: Int, height: Int) : LivenessInfo? {
|
|
|
- return faceEngine?.run {
|
|
|
- val pcode = process(nv21, width, height, FaceEngine.CP_PAF_NV21, listOf(faceInfo), FaceEngine.ASF_LIVENESS)
|
|
|
- val livenesses = ArrayList<LivenessInfo>()
|
|
|
- if (pcode == ErrorInfo.MOK) {
|
|
|
- val lcode = getLiveness(livenesses)
|
|
|
- if (lcode == ErrorInfo.MOK && livenesses.size > 0) {
|
|
|
- return livenesses[0]
|
|
|
- } else {
|
|
|
- Log.d("face", "提取 RGB 活体信息失败:$lcode")
|
|
|
- }
|
|
|
- } else {
|
|
|
- Log.i("face", "人脸属性检测失败:$pcode")
|
|
|
- }
|
|
|
- return null
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 人脸特征提取
|
|
|
- */
|
|
|
- fun getFaceFeature(nv21: ByteArray, faceInfo: FaceInfo, width: Int, height: Int) : FaceFeature? {
|
|
|
- return faceEngine?.run {
|
|
|
- val faceFeature = FaceFeature()
|
|
|
- val code = extractFaceFeature(nv21, width, height, FaceEngine.CP_PAF_NV21, faceInfo, faceFeature)
|
|
|
- if (code == ErrorInfo.MOK) {
|
|
|
- return faceFeature
|
|
|
- } else {
|
|
|
- Log.i("face", "人脸特征提取失败:$code")
|
|
|
- }
|
|
|
- return null
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 在特征库中搜索最相似的一个
|
|
|
- * 原名:getTopOfFaceLib
|
|
|
- * @param feature 传入特征数据
|
|
|
- * @return 比对结果
|
|
|
- */
|
|
|
-// fun findMostSimilarFace(feature: FaceFeature): FaceResultBean? {
|
|
|
-// if (faceDataList.size == 0) {
|
|
|
-// return null
|
|
|
-// }
|
|
|
-// return faceEngine?.run {
|
|
|
-// val tmpFeature = FaceFeature()
|
|
|
-// val faceSimilar = FaceSimilar()
|
|
|
-// var maxSimilarScore = 0f
|
|
|
-// var maxSimilarIndex = -1
|
|
|
-// for (i in faceDataList.indices) {
|
|
|
-// tmpFeature.featureData = faceDataList[i].featureData
|
|
|
-//
|
|
|
-// compareFaceFeature(feature, tmpFeature, faceSimilar)
|
|
|
-//
|
|
|
-// if (faceSimilar.score > maxSimilarScore) {
|
|
|
-// maxSimilarScore = faceSimilar.score
|
|
|
-// maxSimilarIndex = i
|
|
|
-// }
|
|
|
-// }
|
|
|
-// return if (maxSimilarIndex > -1) {
|
|
|
-// FaceResultBean(faceDataList[maxSimilarIndex].name, maxSimilarScore)
|
|
|
-// } else null
|
|
|
-// }
|
|
|
-// }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取面部图像
|
|
|
- */
|
|
|
- fun getFaceBitmap(nv21: ByteArray, width: Int, height: Int, faceInfo: FaceInfo): Bitmap? {
|
|
|
- if (width % 4 != 0 || nv21.size != width * height * 3 / 2) {
|
|
|
- Log.e("face", "invalid params")
|
|
|
- return null
|
|
|
- }
|
|
|
- return faceEngine?.run {
|
|
|
- // 保存注册结果(注册图、特征数据)
|
|
|
- // 为了美观,扩大rect截取注册图
|
|
|
- val cropRect = getBestRect(width, height, faceInfo.rect)
|
|
|
- if (cropRect == null) {
|
|
|
- Log.e("face", "registerNv21: cropRect is null!")
|
|
|
- return null
|
|
|
- }
|
|
|
- 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()
|
|
|
- // 创建一个头像的 Bitmap,存放旋转结果图
|
|
|
- return getHeadImage(nv21, width, height, faceInfo.orient, cropRect)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 保存人脸特征
|
|
|
- */
|
|
|
-// fun saveFeature(feature: FaceFeature, name: String): Boolean {
|
|
|
-// synchronized(this) {
|
|
|
-// //特征存储的文件夹
|
|
|
-// val featureDir = extFile(NEW_FEATURE_DIR)
|
|
|
-// if (!featureDir.exists() && !featureDir.mkdirs()) {
|
|
|
-// Log.e("face", "registerNv21: can not create feature directory")
|
|
|
-// return false
|
|
|
-// }
|
|
|
-// try {
|
|
|
-// val fosFeature = FileOutputStream(featureDir.path + File.separator + name)
|
|
|
-// fosFeature.write(feature.featureData)
|
|
|
-// fosFeature.close()
|
|
|
-// //内存中的数据同步
|
|
|
-// faceDataList.add(FaceBean(name, feature.featureData))
|
|
|
-// return true
|
|
|
-// } catch (e: IOException) {
|
|
|
-// Log.e("face", "保存图片特征异常", e)
|
|
|
-// return false
|
|
|
-// }
|
|
|
-// }
|
|
|
-// }
|
|
|
-
|
|
|
- /**
|
|
|
- * 截取合适的头像并旋转,保存为注册头像
|
|
|
- *
|
|
|
- * @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 = ArcSoftImageFormat.NV21
|
|
|
- ): 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 = Math.max(
|
|
|
- -rect.left,
|
|
|
- Math.max(
|
|
|
- -rect.top,
|
|
|
- Math.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 = Math.min(
|
|
|
- Math.min(
|
|
|
- Math.min(rect.left, width - rect.right),
|
|
|
- height - rect.bottom
|
|
|
- ),
|
|
|
- rect.top
|
|
|
- )
|
|
|
- }
|
|
|
- rect.inset(-padding, -padding)
|
|
|
- return rect
|
|
|
- }
|
|
|
-
|
|
|
- fun optimalOutputSize(cameraManager: CameraManager,cameraId: String, width: Int, height: Int) : Size {
|
|
|
- return cameraManager.getCameraCharacteristics(cameraId).get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
|
|
|
- ?.getOutputSizes(ImageReader::class.java)
|
|
|
- ?.minByOrNull {
|
|
|
- (it.width - height) * (it.height - width)
|
|
|
- }
|
|
|
- ?: Size(width, height)
|
|
|
- }
|
|
|
-
|
|
|
- fun getDataByNV21(cropRect:Rect,format:Int,planes:Array<Image.Plane>) : ByteArray {
|
|
|
- val width: Int = cropRect.width()
|
|
|
- val height: Int = cropRect.height()
|
|
|
- val data = ByteArray(width * height * ImageFormat.getBitsPerPixel(format) / 8)
|
|
|
- val rowData = ByteArray(planes[0].rowStride)
|
|
|
- var channelOffset = 0
|
|
|
- var outputStride = 1
|
|
|
- for (i in planes.indices) {
|
|
|
- when (i) {
|
|
|
- 0 -> {
|
|
|
- channelOffset = 0
|
|
|
- outputStride = 1
|
|
|
- }
|
|
|
- 1 -> {
|
|
|
- channelOffset = width * height + 1
|
|
|
- outputStride = 2
|
|
|
- }
|
|
|
- 2 -> {
|
|
|
- channelOffset = width * height
|
|
|
- outputStride = 2
|
|
|
- }
|
|
|
- }
|
|
|
- val buffer = planes[i].buffer
|
|
|
- val rowStride = planes[i].rowStride
|
|
|
- val pixelStride = planes[i].pixelStride
|
|
|
- val shift = if (i == 0) 0 else 1
|
|
|
- val w = width shr shift
|
|
|
- val h = height shr shift
|
|
|
- buffer.position(rowStride * (cropRect.top shr shift) + pixelStride * (cropRect.left shr shift))
|
|
|
- for (row in 0 until h) {
|
|
|
- var length: Int
|
|
|
- if (pixelStride == 1 && outputStride == 1) {
|
|
|
- length = w
|
|
|
- buffer.get(data, channelOffset, length)
|
|
|
- channelOffset += length
|
|
|
- } else {
|
|
|
- length = (w - 1) * pixelStride + 1
|
|
|
- buffer.get(rowData, 0, length)
|
|
|
- for (col in 0 until w) {
|
|
|
- data[channelOffset] = rowData[col * pixelStride]
|
|
|
- channelOffset += outputStride
|
|
|
- }
|
|
|
- }
|
|
|
- if (row < h - 1) {
|
|
|
- buffer.position(buffer.position() + rowStride - length)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- return data
|
|
|
- }
|
|
|
-
|
|
|
-}
|