diff --git a/.gitignore b/.gitignore index a8b0d1d..aa724b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,35 +1,15 @@ -# ---> Android -# Gradle files -.gradle/ -build/ - -# Local configuration file (sdk path, etc) -local.properties - -# Log/OS Files -*.log - -# Android Studio generated files and folders -captures/ -.externalNativeBuild/ -.cxx/ -*.apk -output.json - -# IntelliJ *.iml -.idea/ -misc.xml -deploymentTargetDropDown.xml -render.experimental.xml - -# Keystore files -*.jks -*.keystore - -# Google Services (e.g. APIs or Firebase) -google-services.json - -# Android Profiling -*.hprof - +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..5ce135b --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,72 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.sw.face.collect" + compileSdk = 36 + + defaultConfig { + applicationId = "com.sw.face.collect" + minSdk = 30 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + ndk { + abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/)) + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + viewBinding { + enable = true + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.activity) + implementation(libs.androidx.constraintlayout) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + // 协程 + implementation(libs.kotlinx.coroutines.android) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + + // camerax + implementation(libs.androidx.camera.core) + implementation(libs.androidx.camera.camera2) + implementation(libs.androidx.camera.lifecycle) + implementation(libs.androidx.camera.view) + implementation(libs.androidx.camera.extensions) + + // 权限申请 + implementation(libs.accompanist.permissions) + implementation(project(":lib_face")) + + implementation("com.github.Dimezis:BlurView:version-3.2.0") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9ce4fef --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/MainActivity.kt b/app/src/main/java/com/sw/face/collect/MainActivity.kt new file mode 100644 index 0000000..26549fa --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/MainActivity.kt @@ -0,0 +1,738 @@ +package com.sw.face.collect + +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Point +import android.hardware.Camera +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver +import android.widget.FrameLayout +import androidx.activity.viewModels +import androidx.core.content.ContextCompat +import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope +import com.arcsoft.face.ErrorInfo +import com.sw.face.collect.base.BaseActivity +import com.sw.face.collect.base.GlobalData +import com.sw.face.collect.databinding.ActivityMainBinding +import com.sw.face.collect.ext.dp +import com.sw.face.collect.ext.gone +import com.sw.face.collect.ext.toast +import com.sw.face.collect.ext.visible +import com.sw.face.collect.socket.LanServer +import com.sw.face.collect.socket.TcpClient +import com.sw.face.collect.utils.Base64 +import com.sw.face.collect.utils.BitmapUtils +import com.sw.face.collect.utils.FaceEngineUtils +import com.sw.face.collect.utils.IntervalExecutor +import com.sw.face.collect.utils.countDownByFlow +import com.sw.face.collect.view.LanServerListenerImpl +import com.sw.face.collect.view.TcpClientListenerImpl +import com.sw.plate.utils.L +import com.sw.plate.utils.NV21ToBitmap +import com.sw.plate.utils.ToastUtils +import com.sw.plate.utils.arcface.ConfigUtil +import com.sw.plate.utils.arcface.ErrorCodeUtil +import com.sw.plate.utils.arcface.FaceRectTransformer +import com.sw.plate.utils.arcface.FaceRectView +import com.sw.plate.utils.arcface.FaceRectView.DrawInfo +import com.sw.plate.utils.arcface.PreviewConfig +import com.sw.plate.utils.arcface.camera.CameraListener +import com.sw.plate.utils.arcface.camera.DualCameraHelper +import com.sw.plate.utils.arcface.face.constants.LivenessType +import com.sw.plate.utils.arcface.face.model.CompareResult +import com.sw.plate.utils.arcface.face.model.FacePreviewInfo +import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration +import com.sw.plate.utils.arcface.model.UserFaceInfo +import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel +import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel.REGISTER_STATUS_READY +import kotlinx.coroutines.Job +import kotlinx.coroutines.runBlocking +import org.json.JSONObject +import kotlin.system.exitProcess + +class MainActivity : BaseActivity(), ViewTreeObserver.OnGlobalLayoutListener { + // private val debouncer = Debouncer(2000) + + companion object { + private const val TAG = "MainActivity" + } + + // 虹软人脸配置 ⬇ + private var isRecognition = false + private var rgbCameraHelper: DualCameraHelper? = null + private var rgbFaceRectTransformer: FaceRectTransformer? = null + + private var livenessType = LivenessType.IR + + // private val livenessType = LivenessType.RGB + private var openRectInfoDraw = false + + private val nv21ToBitmap by lazy { NV21ToBitmap(this) } + + // + private var irCameraHelper: DualCameraHelper? = null + private var irFaceRectTransformer: FaceRectTransformer? = null + + private val recognizeViewModel: RecognizeViewModel by lazy { viewModels().value } + + override fun inflateViewBinding() = ActivityMainBinding.inflate(layoutInflater) + + override fun onGlobalLayout() { + Log.d(TAG, "onGlobalLayout") + binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this) + openCamera() + } + + override fun initialize() { + super.initialize() + FaceEngineUtils.activeEngine() + setupArcCamera() + binding.btnCollectFace.setOnClickListener { + requestSinglePermissionResult(Manifest.permission.CAMERA) { granted -> + if (granted.not()) { + toast("没有相机权限") + return@requestSinglePermissionResult + } + collectFace() + } + } + binding.tvTitle.setOnClickListener { finish() } + addSocketListener() + + binding.flFace.visible() + binding.flRecognizeIr.visible() + binding.flCameraView.visible() + binding.tvFaceTip.visible() + binding.layoutState.gone() + + resumeCamera() + } + + + private fun openCamera() { + Log.d(TAG, "openCamera") + if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != + PackageManager.PERMISSION_GRANTED + ) { + toast("无摄像头权限") + return + } + try { +// val cameraCount = Camera.getNumberOfCameras() +// if (cameraCount < 3) { +// toast("摄像头数量异常"+cameraCount) +// return +// } + recognizeViewModel.init( + PreviewConfig( + PreviewConfig.DEFAULT_IR_CAMERA_ID, PreviewConfig.DEFAULT_RGB_CAMERA_ID, + 0, 0 + ) + ) + initRgbCamera() + if (DualCameraHelper.hasDualCamera() && livenessType === LivenessType.IR) { + initIrCamera() + } + } catch (e: Exception) { + Log.d(TAG, "打开摄像头失败") + toast("打开摄像头失败+${e.message}") + } + } + + + /** + * 启用虹软人脸识别 + */ + fun setupArcCamera() { + initArcViewModel() + initArcView() + openRectInfoDraw = true + } + + private fun sendFaceData(faceData: String) { + // 给某个客户端发送 + val jsonObject = JSONObject().also { + it.put("type", "faceFeature") + it.put("content", faceData) + } + tcpClient?.send(jsonObject) + } + + private var tcpClient: TcpClient? = null + + private fun addSocketListener() { + //val ipAddress = NetworkUtils.getIPAddress(true) + //Log.d(TAG, "addSocketListener,ipAddress: $ipAddress"); + tcpClient = TcpClient( + "192.168.1.95", + 5000, + GlobalData.deviceId, // clientId + 5000, // connectTimeoutMs + 10 * 1000, // heartbeatIntervalMs + 30 * 1000 // heartbeatTimeoutMs + ) + + tcpClient?.setListener(object : TcpClientListenerImpl() { + override fun onSendSuccess(json: JSONObject?) { + super.onSendSuccess(json) +// toast("发送成功") + } + + override fun onSendFailed(json: JSONObject?, e: java.lang.Exception?) { + super.onSendFailed(json, e) + toast("发送失败${e?.message}") + } + }) + tcpClient?.start(); + + try { + LanServer.getInstance().let { + it.setListener(object : LanServerListenerImpl() { + override fun onMessageReceived(clientId: String?, message: JSONObject?) { + super.onMessageReceived(clientId, message) + toast("收到消息clientId=$clientId") + } + }) + it.start() + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + // private var imageBitmap: Bitmap? = null + private fun collectFace() { + userFaceInfo = null + recognizeViewModel.updateRegisterStatus(REGISTER_STATUS_READY) + //showWaitingDialog("人脸信息中……") + + binding.tvFaceTip.gone() + binding.btnCollectFace.gone() + binding.layoutState.visible() + binding.pbCollectLoading.visible() + binding.tvCollectState.text = "采集中......" + getFaceData { faceData -> + Log.d(TAG, "faceData: $faceData") + runOnUiThread { + //hideWaitingDialog() + //toast("人脸信息已采集") + binding.pbCollectLoading.gone() + //binding.tvCollectState.text = "采集完成" + userFaceInfo = null + countDown() + } + sendFaceData(faceData) + } + } + + private var job: Job?=null + + private fun getFaceData(block: (String) -> Unit) { + runBlocking { + job = executor.startIntervalTask(5) { + if (userFaceInfo == null) { + return@startIntervalTask + } + val faceData = Base64.encode(userFaceInfo!!.faceFeature!!.featureData) + block(faceData) + job?.cancel() + } + } + + } + + val executor = IntervalExecutor() + + + public override fun onStop() { + rgbCameraHelper?.release() + rgbCameraHelper = null + irCameraHelper?.release() + irCameraHelper = null + recognizeViewModel.destroy() + super.onStop() + } + + fun resumeCamera() { + Log.d(TAG, "resumeCamera isRecognition = $isRecognition") + isRecognition = true + if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) { + rgbCameraHelper!!.start() + } + } + + fun pauseCamera() { + Log.d(TAG, "pauseCamera isRecognition = $isRecognition") + isRecognition = false + + recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) + } + + private var userFaceInfo: UserFaceInfo? = null + private var isCheckCamera = false + private fun initArcViewModel() { + if (DualCameraHelper.hasDualCamera()) { + livenessType = LivenessType.IR + isCheckCamera = true + } else { + livenessType = LivenessType.RGB + isCheckCamera = false + } + recognizeViewModel.setLiveType(livenessType) + + recognizeViewModel.prepareRegister() + recognizeViewModel.setOnRegisterFinishedCallback { facePreviewInfo, userFaceInfo -> + this.userFaceInfo = userFaceInfo + Log.d(TAG, "initArcViewModel: userFaceInfo == null : ${userFaceInfo == null}") + } + recognizeViewModel.ftInitCode.observe(this, Observer { ftInitCode: Int? -> + if (ftInitCode != ErrorInfo.MOK) { + val error: String? = context.getString( + R.string.specific_engine_init_failed, "ftEngine", + ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode!!) + ) + Log.d(TAG, "ftInitCode observe = $error") + ToastUtils.showToast(error) + } + }) + recognizeViewModel.frInitCode.observe(this, Observer { frInitCode: Int? -> + if (frInitCode != ErrorInfo.MOK) { + val error: String? = context.getString( + R.string.specific_engine_init_failed, "frEngine", + frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode!!) + ) + Log.d(TAG, "frInitCode observe = $error") + ToastUtils.showToast(error) + } + }) + recognizeViewModel.flInitCode.observe(this, Observer { flInitCode: Int? -> + if (flInitCode != ErrorInfo.MOK) { + val error: String? = context.getString( + R.string.specific_engine_init_failed, "flEngine", + flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode!!) + ) + Log.d(TAG, "flInitCode observe = $error") + ToastUtils.showToast(error) + } + }) + + recognizeViewModel.recognizeConfiguration + .observe(this, Observer { recognizeConfiguration: RecognizeConfiguration? -> + Log.d(TAG, "recognizeConfiguration: observe = ${recognizeConfiguration.toString()}") + }) + recognizeViewModel.recognizeNotice.observe(this, Observer { notice: String? -> + Log.d(TAG, "recognizeNotice observe notice = $notice") + }) + + recognizeViewModel.recognizeUserId.observe( + this, + Observer { compareResult: CompareResult -> + Log.d( + TAG, + "recognizeUserId observe compareResult = ${compareResult.trackId}, userId = ${compareResult.faceEntity.userName}" + ) +// val lastFaceTrackId = compareResult.trackId +// val faceEntity = compareResult.faceEntity +// val currentUserId = faceEntity.userName +// if (!currentUserId.isNullOrBlank()) { +// toast("当前用户已采集过人脸信息") +// return@Observer +// } +// //未识别到,拍摄照片 + + }) + + recognizeViewModel.drawRectInfoText.observe(this, Observer { info -> + Log.d(TAG, "drawRectInfoText observe info = $info") + }) + } + + private fun initArcView() { + //在布局结束后才做初始化操作 + binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(this) + recognizeViewModel.getCompareResultList().getValue() + } + + /** + * 调整View的宽高,使预览显示正常且采集框固定为 + * + * @param rgbPreview RGB预览View + * @param previewView 显示预览数据的view + * @param faceRectView 画框的view + * @param previewSize 预览大小 + * @param displayOrientation 相机旋转角度 + * @param scale 缩放比例 + * @return 调整后的LayoutParams + */ + private fun adjustPreviewViewSize( + rgbPreview: View, + previewView: View, + faceRectView: FaceRectView, + previewSize: Camera.Size, + displayOrientation: Int, + scale: Float + ): ViewGroup.LayoutParams { +// val w = 640.dp//300.dp + val w = 640.dp//300.dp + val h = 480.dp//510.dp + val layoutParams = FrameLayout.LayoutParams(w, h); + previewView.setLayoutParams(layoutParams) + faceRectView.setLayoutParams(layoutParams) + + return layoutParams + } + + private fun initRgbCamera() { + val cameraListener: CameraListener = object : CameraListener { + override fun onCameraOpened( + camera: Camera, + cameraId: Int, + displayOrientation: Int, + isMirror: Boolean + ) { + Log.d( + TAG, + "initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation" + ) + runOnUiThread({ + val previewSizeRgb = camera.getParameters().getPreviewSize() + val layoutParams = adjustPreviewViewSize( + binding.dualCameraTexturePreviewRgb, + binding.dualCameraTexturePreviewRgb, + binding.dualCameraFaceRectView, + previewSizeRgb, displayOrientation, + 0.5F + //0.6F + ) + + Log.d( + TAG, + "initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}" + ) + Log.d( + TAG, + "initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}" + ) + Log.d( + TAG, + "initRgbCamera isMirror = ${isMirror}, isDrawRgbRectHorizontalMirror = ${ + ConfigUtil.isDrawRgbRectHorizontalMirror( + context + ) + }, isDrawRgbRectVerticalMirror = ${ + ConfigUtil.isDrawRgbRectVerticalMirror( + context + ) + }" + ) + // 调整识别窗口位置 + rgbFaceRectTransformer = FaceRectTransformer( + previewSizeRgb.width, + previewSizeRgb.height, + layoutParams.width, + layoutParams.height, + displayOrientation, +// 90, + cameraId, + isMirror, + true, + false + ) + + recognizeViewModel.onRgbCameraOpened(camera) + recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer) + }) + } + + override fun onPreview(nv21: ByteArray?, camera: Camera?) { +// faceByteArray = nv21 + if (!isRecognition) { + return + } + if (isCheckCamera) { + L.e("检查摄像头数据") + + isCheckCamera = false + + val isBw: Boolean = BitmapUtils.isBlackWhite( + nv21ToBitmap.nv21ToBitmap( + nv21, + rgbFaceRectTransformer!!.previewWidth, + rgbFaceRectTransformer!!.previewHeight, + 0 + ) + ) + if (isBw) { + L.e("黑白切换摄像头") + switchCamera() + } + L.e("摄像头正常") + } + binding.dualCameraFaceRectView.clearFaceInfo() + val facePreviewInfoList: MutableList? = + recognizeViewModel.onPreviewFrame(nv21, true) + if (facePreviewInfoList != null + && rgbFaceRectTransformer != null + ) { + drawPreviewInfo(facePreviewInfoList) + } + recognizeViewModel.clearLeftFace(facePreviewInfoList) + } + + override fun onCameraClosed() { + Log.d(TAG, "initRgbCamera onCameraClosed: ") + } + + override fun onCameraError(e: java.lang.Exception) { + Log.d(TAG, "initRgbCamera onCameraError: ${e.message}") + e.printStackTrace() + } + + override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) { + Log.d( + TAG, + "initRgbCamera onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}" + ) + if (rgbFaceRectTransformer != null) { + rgbFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation + } + Log.d( + TAG, + "initRgbCamera onCameraConfigurationChanged: $cameraID $displayOrientation" + ) + } + } + val measuredWidth = binding.dualCameraTexturePreviewRgb.measuredWidth + val measuredHeight = binding.dualCameraTexturePreviewRgb.measuredHeight + Log.d(TAG, "initRgbCamera measuredWidth=$measuredWidth,measuredHeight=$measuredHeight") + + val previewConfig = recognizeViewModel.previewConfig + rgbCameraHelper = DualCameraHelper.Builder() + .previewViewSize(Point(measuredWidth, measuredHeight)) +// .previewViewSize(Point(608.dp, 456.dp)) +// .previewViewSize(Point(1024, 768)) + .rotation(windowManager.defaultDisplay.rotation) + .additionalRotation(previewConfig.rgbAdditionalDisplayOrientation) // 角度 + .previewSize(recognizeViewModel.loadPreviewSize()) +// .previewSize(Point(1080, 720)) + .specificCameraId(previewConfig.rgbCameraId) + .isMirror(true) + .previewOn(binding.dualCameraTexturePreviewRgb) + .cameraListener(cameraListener) + .build() + rgbCameraHelper?.run { + init() +// setSurfaceFrameCallback { bitmap -> +// imageBitmap = BitmapFlipUtil.flipHorizontally(bitmap) +// +//// binding.ivRecImage.visible() +//// binding.ivRecImage.setImageBitmap(imageBitmap) +// } + start() + } + } + + /** + * 初始化红外相机,若活体检测类型是可见光活体检测或不启用活体,则不需要启用 + */ + private fun initIrCamera() { + Log.d(TAG, "initIrCamera: livenessType = $livenessType") + if (livenessType === LivenessType.RGB) { + return + } + val irCameraListener: CameraListener = object : CameraListener { + override fun onCameraOpened( + camera: Camera, + cameraId: Int, + displayOrientation: Int, + isMirror: Boolean + ) { + Log.d( + TAG, + "initIrCamera IR onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation" + ) + val previewSizeIr = camera.getParameters().getPreviewSize() + val layoutParams = adjustPreviewViewSize( + binding.dualCameraTexturePreviewRgb, + binding.dualCameraTexturePreviewIr, binding.dualCameraFaceRectViewIr, + previewSizeIr, displayOrientation, 0.25f + ) + + irFaceRectTransformer = FaceRectTransformer( + previewSizeIr.width, previewSizeIr.height, +// layoutParams.width, layoutParams.height, + layoutParams.width, layoutParams.height, + displayOrientation, cameraId, isMirror, + ConfigUtil.isDrawIrRectHorizontalMirror(context), + ConfigUtil.isDrawIrRectVerticalMirror(context) + ) + + recognizeViewModel.onIrCameraOpened(camera) + recognizeViewModel.setIrFaceRectTransformer(irFaceRectTransformer) + } + + + override fun onPreview(nv21: ByteArray?, camera: Camera?) { + recognizeViewModel.refreshIrPreviewData(nv21) + } + + override fun onCameraClosed() { + Log.d(TAG, "initIrCamera onCameraClosed: ") + } + + override fun onCameraError(e: java.lang.Exception) { + Log.d(TAG, "initIrCamera onCameraError: ${e.message}") + e.printStackTrace() + } + + override fun onCameraConfigurationChanged( + cameraID: Int, + displayOrientation: Int + ) { + if (irFaceRectTransformer != null) { + irFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation + } + Log.d( + TAG, + "initIrCamera onCameraConfigurationChanged: cameraID = $cameraID, displayOrientation = $displayOrientation" + ) + } + } + + val previewConfig = recognizeViewModel.previewConfig + irCameraHelper = DualCameraHelper.Builder() + .previewViewSize( + Point( + binding.dualCameraTexturePreviewIr.measuredWidth, + binding.dualCameraTexturePreviewIr.measuredHeight + ) + ) +// .previewViewSize(Point(4,3)) + .rotation(windowManager.defaultDisplay.rotation) + .specificCameraId(previewConfig.irCameraId) + .previewOn(binding.dualCameraTexturePreviewIr) + .cameraListener(irCameraListener) + .isMirror(true) + .previewSize(recognizeViewModel.loadPreviewSize()) //相机预览大小设置,RGB与IR需使用相同大小 + .additionalRotation(previewConfig.irAdditionalDisplayOrientation) //额外旋转角度 + .build() + irCameraHelper?.init() + try { + irCameraHelper?.start() + } catch (e: RuntimeException) { + ToastUtils.showToast(e.message + context.getString(R.string.camera_error_notice)) + } + } + + /** + * 绘制RGB、IR画面的实时人脸信息 + * + * @param facePreviewInfoList RGB画面的实时人脸信息 + */ + private fun drawPreviewInfo(facePreviewInfoList: MutableList) { +// Timber.tag(TAG).d("drawPreviewInfo facePreviewInfoList = ${facePreviewInfoList.size}, rgbFaceRectTransformer = ${rgbFaceRectTransformer != null}") + if (rgbFaceRectTransformer != null) { + val rgbDrawInfoList: MutableList? = recognizeViewModel.getDrawInfo( + facePreviewInfoList, + LivenessType.RGB, + openRectInfoDraw + ) + // 识别成功 + binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList) + } + +// val listIsEmpty = facePreviewInfoList.isEmpty() +// val listFirstTrackId = if (listIsEmpty.not()) facePreviewInfoList[0].trackId else null +// Log.d(TAG,"listIsEmpty=$listIsEmpty,lastFaceTrackId=$lastFaceTrackId,listFirstTrackId=$listFirstTrackId") +// if (listIsEmpty || (lastFaceTrackId != listFirstTrackId)) { +// if (lastFaceTrackId != -1 && currentUserId != null) { +// mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0 +// Timber.tag(com.sw.dualscreen.presentation.MainScreenPresentation.Companion.TAG).i("$lastFaceTrackId 用户离开") +// lastFaceTrackId = -1 +// Timber.tag(com.sw.dualscreen.presentation.MainScreenPresentation.Companion.TAG) +// .d("postUserData userNutritionData是否null: ${userNutrition == null}, currentFood是否null: ${currentFood == null},currentUserId是否null:${currentUserId == null}") +// if (userNutrition == null || currentFood == null || currentUserId == null) { +// return +// } +// postUserData() +// if (mealPickupMode == 1 || isKeepFaceState) { +// step2FaceRecognizing(currentFood!!) +// isKeepFaceState = false +// } else { +// activity.isAnalyzing = false +// step1FoodRecognizing() +// } +// } +// } + } + + fun switchCamera() { + try { + L.e("开始切换摄像头") + if (rgbCameraHelper != null && irCameraHelper != null) { + rgbCameraHelper!!.stop() + irCameraHelper!!.stop() + rgbCameraHelper!!.switchCameraId() + irCameraHelper!!.switchCameraId() + rgbCameraHelper!!.start() + irCameraHelper!!.start() + rgbFaceRectTransformer!!.cameraId = + if (rgbFaceRectTransformer!!.getCameraId() === 1) 0 else 1 + // showLongToast(getString(R.string.notice_change_detect_degree)); + L.e("切换摄像头成功") + } else { + L.e("切换摄像头失败") + + // showToast(getString(R.string.switch_camera_failed)); + exit() + val newIntent = Intent(this, MainActivity::class.java) // 要启动的Activity + newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(newIntent) + } + } catch (e: java.lang.RuntimeException) { + L.e("切换摄像头失败") + toast(e.message + getString(R.string.camera_error_notice)) + + exit() + val newIntent = Intent(this, MainActivity::class.java) // 要启动的Activity + newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(newIntent) + } + } + + private fun exit() { + finish() + exitProcess(0) + } + + override fun onDestroy() { + countDownJob?.cancel() + super.onDestroy() + } + + private var countDownJob: Job? = null + private fun countDown() { + countDownJob = countDownByFlow( + total = 3, + scope = lifecycleScope, + onStart = { + binding.tvCollectState.text = "采集完成(3s)" + }, + onTick = { seconds -> + binding.tvCollectState.text = "采集完成(${seconds}s)" + }, + onFinish = { + //binding.tvCollectState.text = "采集完成" + + binding.layoutState.gone() + binding.tvFaceTip.visible() + binding.btnCollectFace.visible() + } + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/MainActivityBak.kt b/app/src/main/java/com/sw/face/collect/MainActivityBak.kt new file mode 100644 index 0000000..e04e774 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/MainActivityBak.kt @@ -0,0 +1,163 @@ +//package com.sw.face.collect +// +//import android.Manifest +//import android.net.Uri +//import android.util.Log +//import android.view.View +//import androidx.camera.view.PreviewView +//import androidx.core.view.updateLayoutParams +//import com.sw.face.collect.base.BaseActivity +//import com.sw.face.collect.databinding.ActivityMainBakBinding +//import com.sw.face.collect.databinding.LayoutCameraPreviewBinding +//import com.sw.face.collect.ext.dp +//import com.sw.face.collect.ext.gone +//import com.sw.face.collect.ext.toast +//import com.sw.face.collect.ext.visible +//import com.sw.face.collect.socket.LanCommunicationManager +//import com.sw.face.collect.socket.LanServer +//import com.sw.face.collect.utils.Base64 +//import com.sw.face.collect.utils.CameraUtils +//import com.sw.face.collect.utils.ToastUtils +//import org.json.JSONException +//import org.json.JSONObject +// +//class MainActivityBak : BaseActivity() { +// // private val debouncer = Debouncer(2000) +// +// companion object { +// private const val TAG = "MainActivity" +// } +// +// private lateinit var previewView: PreviewView +// private val cameraUtils: CameraUtils by lazy { +// CameraUtils(this) +// } +// +// override fun inflateViewBinding() = ActivityMainBakBinding.inflate(layoutInflater) +// +// override fun initialize() { +// super.initialize() +// cameraUtils.initCamera() +// val previewBinding = +// LayoutCameraPreviewBinding.inflate(layoutInflater, binding.flCameraPreview) +// previewView = previewBinding.previewView.also { +// it.updateLayoutParams { +// width = 680.dp +// height = 500.dp +// } +// } +// cameraUtils.setPreviewController(previewView) +// +// binding.btnClear.setOnClickListener { +// binding.ivFaceImage.gone() +// binding.flCameraPreview.visible() +// } +// binding.btnTakePhoto.setOnClickListener { +// requestSinglePermissionResult(Manifest.permission.CAMERA) { granted -> +// if (granted.not()) { +// toast("没有相机权限") +// return@requestSinglePermissionResult +// } +// takePhoto() +// } +// } +// binding.btnUpload.setOnClickListener { +// getFaceData { faceData -> +// Log.d(TAG, "faceData: $faceData") +// sendFaceData(faceData) +// } +// } +// binding.tvTitle.setOnClickListener { finish() } +// addSocketListener() +// } +// +// private fun sendFaceData(faceData: String) { +// // 给某个客户端发送 +// val jsonObject = JSONObject().also { +// it.put("type", "cmd") +// it.put("content", faceData) +// } +// val clientId = "device12231530" +// lanManager?.sendToClient(clientId, jsonObject) +// lanManager?.broadcast(jsonObject) +// } +// +// private var lanManager: LanCommunicationManager?=null +// private fun addSocketListener() { +// lanManager = LanServer.getInstance() +// lanManager?.setListener(object : LanCommunicationManager.Listener{ +// override fun onClientConnected(clientId: String?) { +// Log.d(TAG, "addSocketListener,onClientConnected: clientId=$clientId") +// } +// +// override fun onClientDisconnected(clientId: String?) { +// Log.d(TAG, "addSocketListener,onClientDisconnected: clientId=$clientId") +// } +// +// override fun onMessageReceived( +// clientId: String?, +// message: JSONObject? +// ) { +// Log.d(TAG, "addSocketListener,onMessageReceived: clientId=$clientId,message=$message") +// } +// }) +// try { +// lanManager?.start() +// } catch (e: Exception) { +// e.printStackTrace() +// } +// } +// +// private fun takePhoto() { +// showWaitingDialog("拍照中……") +// cameraUtils.takePhoto(cameraCallback) { errMsg -> +// hideWaitingDialog() +// ToastUtils.showToast("拍照异常,请重新操作") +// } +// } +// +// private var imageUri: Uri? = null +// private fun getFaceData(block: (String) -> Unit) { +// if (imageUri == null) { +// toast("未采集到照片,请拍照") +// return +// } +// context.contentResolver.openInputStream(imageUri!!)?.use { stream -> +// val faceData = Base64.encode(stream.readBytes()) +// block(faceData) +// } +// } +// +// private val cameraCallback: (Uri) -> Unit = { uri -> +// try { +// this.imageUri = uri +// binding.flCameraPreview.gone() +// binding.ivFaceImage.run { +// visible() +// setImageURI(uri) +// } +// hideWaitingDialog() +// } catch (e: Exception) { +// e.printStackTrace() +// hideWaitingDialog() +// ToastUtils.showToast("程序异常${e.message}") +// } +// } +// +// override fun onResume() { +// super.onResume() +// cameraUtils.bind() +// binding.llCameraFlag.run { +// visibility = View.VISIBLE +// postDelayed({ +// visibility = View.GONE +// }, 3000) +// } +// } +// +// override fun onPause() { +// super.onPause() +// cameraUtils.unbind() +// binding.llCameraFlag.visibility = View.VISIBLE +// } +//} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/MyApp.kt b/app/src/main/java/com/sw/face/collect/MyApp.kt new file mode 100644 index 0000000..66a9d00 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/MyApp.kt @@ -0,0 +1,25 @@ +package com.sw.face.collect + +import android.annotation.SuppressLint +import android.content.Context +import android.util.Log +import com.sw.face.collect.base.GlobalData +import com.sw.plate.App +import com.sw.plate.utils.AppUtil + +class MyApp: App() { + + companion object { + private const val TAG = "MyApp" + @SuppressLint("StaticFieldLeak") + var instance: Context? = null + } + + override fun onCreate() { + super.onCreate() + instance = this + GlobalData.deviceId = AppUtil.getUDID(this) + Log.d(TAG, "onCreate: deviceId = ${GlobalData.deviceId}") + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/base/BaseActivity.kt b/app/src/main/java/com/sw/face/collect/base/BaseActivity.kt new file mode 100644 index 0000000..b05f4d0 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/base/BaseActivity.kt @@ -0,0 +1,112 @@ +package com.sw.face.collect.base + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.TextView +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.viewbinding.ViewBinding +import com.sw.face.collect.databinding.DialogWaitingBinding +import com.sw.face.collect.view.CustomDialog +import com.sw.face.collect.R +import com.sw.face.collect.model.IActivityResult + +abstract class BaseActivity : AppCompatActivity() { + + protected lateinit var binding: VB + protected lateinit var context: Context + private var mDialogWaiting: CustomDialog? = null + + private lateinit var permissionLauncher: ActivityResultLauncher + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) +// EventBus.getDefault().register(this) + enableEdgeToEdge() + context = this + + //保持亮屏 + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + + binding = inflateViewBinding() + setContentView(binding.root) + initActivityLauncher() + initialize() + } + + override fun onDestroy() { +// EventBus.getDefault().unregister(this) + super.onDestroy() + } + + protected abstract fun inflateViewBinding(): VB + + open fun initialize() { + + } + + + /** + * 显示等待提示框 + */ + fun showWaitingDialog(tip: String?) { + runOnUiThread { + hideWaitingDialog() + val dialogBinding = DialogWaitingBinding.inflate(LayoutInflater.from(this)) + dialogBinding.tvTip.text = tip + mDialogWaiting = CustomDialog(this, dialogBinding.root) + mDialogWaiting?.show() + } + } + + /** + * 隐藏等待提示框 + */ + fun hideWaitingDialog() { + runOnUiThread { + mDialogWaiting?.dismiss() + mDialogWaiting = null + } + } + + fun showWaitingDialog2(tip: String?) { + if (mDialogWaiting == null) { + hideWaitingDialog() + val view = View.inflate(this, R.layout.dialog_waiting, null) + mDialogWaiting = CustomDialog(this, view, R.style.MyDialog) + mDialogWaiting?.show() + } + val contentView = mDialogWaiting?.findViewById(android.R.id.content) + val tvTip = contentView?.findViewById(R.id.tvTip) + tvTip?.text = tip + } + + private var launchPermissionCallback: ((Boolean) -> Unit)? = null + + fun requestSinglePermissionResult( + permission: String, + callback: (Boolean) -> Unit + ) { + this.launchPermissionCallback = callback + permissionLauncher.launch(permission) + } + + private fun initActivityLauncher() { + permissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + launchPermissionCallback?.invoke(isGranted) + } + } + +// @Subscribe(threadMode = ThreadMode.MAIN) +// fun onPostEvent(event: PostEvent) { +// +// } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/base/GlobalData.kt b/app/src/main/java/com/sw/face/collect/base/GlobalData.kt new file mode 100644 index 0000000..cb82117 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/base/GlobalData.kt @@ -0,0 +1,60 @@ +package com.sw.face.collect.base + +object GlobalData { + + /** + * 设备id + */ + var deviceId: String = "" + + /** + * app版本号 + */ + var appVersion: String = "1" + + /** + * 具体业务baseurl + */ + var appBaseUrl: String = "" + + /** + * 横排数量 + */ + var arrayCross: Int = 2 + + /** + * 竖排数量 + */ + var arrayVertical: Int = 11 + + /** + * 排列方式 0 垂直 1 水平 + */ + var arrayMode: Int = 0 + + var appId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj" + var sdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k" + var activeKey = "085F-118G-Q3WA-8QT4" +} + +/** + * 全局常量 + */ +object Constants { + /** + * 自动关闭时间 + */ + const val AUTO_CLOSE_TIME: Long = 3 +} + +/** + * key + */ +object GlobalKey { + const val KEY_EQUIPMENT_INFO = "equipmentInfo" + const val KEY_TOKEN = "tokenKey" + const val KEY_FIRST_RUN = "firstRun" + const val KEY_USER_INFO = "userInfoKey" + const val KEY_PICKUP_MODE = "pickupMode" + const val KEY_CHARGE_MODE = "chargeMode" +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/ext/Common.kt b/app/src/main/java/com/sw/face/collect/ext/Common.kt new file mode 100644 index 0000000..d1e66a3 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/ext/Common.kt @@ -0,0 +1,252 @@ +package com.sw.face.collect.ext + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.content.res.Resources +import android.graphics.drawable.GradientDrawable +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.TypedValue +import android.view.View +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import android.widget.Toast +import androidx.core.app.ActivityOptionsCompat +import androidx.core.graphics.toColorInt +import androidx.fragment.app.Fragment +import com.sw.face.collect.utils.ToastUtils +//import com.google.gson.Gson +//import com.google.gson.reflect.TypeToken +//import com.sw.inbound.utils.CustomToastUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.math.BigDecimal +import java.math.RoundingMode + +fun Context.toast( + message: String?, + duration: Int = Toast.LENGTH_SHORT, +) { + if (message.isNullOrBlank()) { + return + } + Handler(Looper.getMainLooper()).post { + //Toast.makeText(this, message, duration).show() + //CustomToastUtils.showToast(message, duration) + ToastUtils.showToast(message,duration) + } +} + +fun Fragment.toast( + message: String?, + duration: Int = 2000 +) { + activity?.toast(message, duration) +} + + +fun View.visible() { + visibility = View.VISIBLE +} + +fun View.invisible() { + visibility = View.INVISIBLE +} + +fun View.gone() { + visibility = View.GONE +} + +inline fun Context.startActivity( + bundle: Bundle? = null, + options: ActivityOptionsCompat? = null +) { + Intent(this, T::class.java).apply { + bundle?.let { putExtras(it) } + if (options != null && this@startActivity is Activity) { + startActivity(this, options.toBundle()) + } else { + startActivity(this) + } + } +} + +inline fun Context.startActivity( + block: Intent.() -> Unit = {} +) { + Intent(this, T::class.java).apply { + block() + startActivity(this) + } +} + +//inline fun Context.startActivity(action:(bundle: Bundle)-> Unit) { +// Intent(this, T::class.java).apply { +// action(Bundle()) +// startActivity(this) +// } +//} + +//inline fun String.toType(gson: Gson? = null, typeToken: TypeToken): T { +// return (gson ?: Gson()).fromJson(this, typeToken.type) +//} +// +//inline fun String.toObject(gson: Gson? = null): T { +// return (gson ?: Gson()).fromJson(this, T::class.java) +//} +// +//fun Any?.toJsonString(gson: Gson? = null): String { +// return (gson ?: Gson()).toJson(this) ?: "" +//} + +@SuppressLint("ApplySharedPref") +inline fun SharedPreferences.edit( + commit: Boolean = false, + action: SharedPreferences.Editor.() -> Unit +) { + val editor = edit() + action(editor) + if (commit) editor.commit() else editor.apply() +} + +fun SharedPreferences.put(vararg pairs: Pair) { + edit { + pairs.forEach { (key, value) -> + when (value) { + is Int -> putInt(key, value) + is String -> putString(key, value) + is Boolean -> putBoolean(key, value) + is Float -> putFloat(key, value) + is Long -> putLong(key, value) + } + } + } +} + +val Float.dp: Float + get() = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + this, + Resources.getSystem().displayMetrics + ) + +val Int.dp: Int + get() = this.toFloat().dp.toInt() + +fun EditText.addOnActionSearchListener(searchCallback: () -> Unit) { + setOnEditorActionListener { v, actionId, event -> + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + // 处理搜索逻辑 + searchCallback() + return@setOnEditorActionListener true // 阻止事件继续传递 + } + return@setOnEditorActionListener false + } +} + + +fun View.clickWithDebounce(delay: Long = 300, action: () -> Unit) { + var job: Job? = null + setOnClickListener { + job?.cancel() + job = CoroutineScope(Dispatchers.Main).launch { + delay(delay) + action() + } + } +} + +fun Double.roundedDecimalPlace(num: Int): Double { + return BigDecimal(this).setScale(num, RoundingMode.HALF_UP).toDouble() +} + +fun Double.roundedOneDecimalPlace(): Double { + return this.roundedDecimalPlace(1) +} + +fun View.setShapeDrawable( + solidColor: String = "#FFFFFF", + strokeWidth: Int = 0, + strokeColor: String = "#FFFFFF", + radius: Int = 0 +) { + background = GradientDrawable().apply { + setColor(solidColor.toColorInt()) + setStroke(strokeWidth, strokeColor.toColorInt()) + cornerRadius = radius.toFloat() + } +} + +fun View.setShapeDrawable2( + solidColor: String = "#FFFFFF", + strokeWidth: Int = 0, + strokeColor: String = "#FFFFFF", + topLeftRadius: Int = 0, + topRightRadius: Int = 0, + bottomRightRadius: Int = 0, + bottomLeftRadius: Int = 0 +) { + background = GradientDrawable().apply { + setColor(solidColor.toColorInt()) + setStroke(strokeWidth, strokeColor.toColorInt()) + + setCornerRadii( + floatArrayOf( + topLeftRadius.toFloat(), topLeftRadius.toFloat(), + topRightRadius.toFloat(), topRightRadius.toFloat(), + bottomRightRadius.toFloat(), bottomRightRadius.toFloat(), + bottomLeftRadius.toFloat(), bottomLeftRadius.toFloat() + ) + ) + } +} + +fun String?.ifNullOrBlank(defaultValue: String): String { + return if (this.isNullOrBlank()) defaultValue else this +} + +fun Int?.ifNullOrZero(defaultValue: String): String { + return if (this == null || this == 0) defaultValue else this.toString() +} + +fun Double?.ifNullOrZero(defaultValue: String): String { + return if (this == null || this == 0.toDouble()) defaultValue else this.toString() +} + +fun View.hideKeyboard() { + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(this.windowToken, 0) + this.clearFocus() // 清除焦点避免键盘再次弹出 +} + +/** + * 复制文本到剪贴板 + * @param context 上下文 + * @param text 要复制的文本内容 + * @param showToast 是否显示复制成功提示,默认为true + */ +fun String.copyText(context: Context, showToast: Boolean = true) { + // 获取剪贴板管理器 + val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + + // 创建ClipData对象,包含要复制的文本 + val clipData = ClipData.newPlainText("label", this) + + // 将数据设置到剪贴板 + clipboardManager.setPrimaryClip(clipData) + + // 显示复制成功提示 + if (showToast) { + Toast.makeText(context, "文本已复制到剪贴板", Toast.LENGTH_SHORT).show() + } +} diff --git a/app/src/main/java/com/sw/face/collect/ext/SpannableExt.kt b/app/src/main/java/com/sw/face/collect/ext/SpannableExt.kt new file mode 100644 index 0000000..f6befaa --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/ext/SpannableExt.kt @@ -0,0 +1,24 @@ +package com.sw.face.collect.ext + +import android.text.SpannableStringBuilder +import android.text.Spanned + + +inline fun buildSpannableString(builderAction: SpannableStringBuilder.() -> Unit): SpannableStringBuilder { + return SpannableStringBuilder().apply(builderAction) +} + +fun SpannableStringBuilder.appendText(text: String, vararg spans: Any): SpannableStringBuilder { + val start = length + append(text) + spans.forEach { span -> + setSpan(span, start, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + } + return this +} + +fun SpannableStringBuilder.withSpan(span: Any, block: SpannableStringBuilder.() -> Unit) { + val start = length + block() + setSpan(span, start, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) +} diff --git a/app/src/main/java/com/sw/face/collect/model/IActivityResult.java b/app/src/main/java/com/sw/face/collect/model/IActivityResult.java new file mode 100644 index 0000000..413e577 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/model/IActivityResult.java @@ -0,0 +1,24 @@ +package com.sw.face.collect.model; + +import androidx.activity.result.ActivityResult; + +import java.util.Map; + +public interface IActivityResult { + + interface LaunchActivityCallback { + void callback(ActivityResult result); + } + + interface LaunchTackPictureCallback { + void callback(boolean result); + } + + interface RequestPermissionCallback { + void callback(boolean result); + } + + interface RequestMultiplePermissionsCallback { + void callback(Map result); + } +} diff --git a/app/src/main/java/com/sw/face/collect/socket/LanCommunicationManager.java b/app/src/main/java/com/sw/face/collect/socket/LanCommunicationManager.java new file mode 100644 index 0000000..c9474c6 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/socket/LanCommunicationManager.java @@ -0,0 +1,300 @@ +package com.sw.face.collect.socket; + +import android.annotation.SuppressLint; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 局域网多客户端通信管理器:支持并发、心跳、认证、广播、点对点发送 + * 可用于 Android 和 JVM 程序。 + */ +public class LanCommunicationManager { + + // ============ 监听配置 ============ + private final int port; + private final long HEARTBEAT_TIMEOUT_MS; + private final int MAX_CLIENT_THREADS; + + // ============ 状态 ============ + private volatile boolean running = false; + private ServerSocket serverSocket; + + // ============ 线程池 ============ + private final ExecutorService acceptExecutor = Executors.newSingleThreadExecutor(); + private final ExecutorService clientExecutor; + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + // ============ 客户端会话 ============ + private final ConcurrentHashMap clients = new ConcurrentHashMap<>(); + private final ConcurrentHashMap unAuthSessions = new ConcurrentHashMap<>(); + + // ============ 回调接口 ============ + public interface Listener { + // 新客户端完成 AUTH / 认证 + void onClientConnected(String clientId); + + // 客户端断开 + void onClientDisconnected(String clientId); + + // 收到业务消息(type != heartbeat/auth) + void onMessageReceived(String clientId, JSONObject message); + } + + private Listener listener; + + // ============ 构造 ============ + public LanCommunicationManager(int port, int maxClientThreads, long heartbeatTimeoutMs) { + this.port = port; + this.MAX_CLIENT_THREADS = maxClientThreads; + this.HEARTBEAT_TIMEOUT_MS = heartbeatTimeoutMs; + this.clientExecutor = Executors.newFixedThreadPool(Math.max(2, maxClientThreads)); + } + + public void setListener(Listener listener) { + this.listener = listener; + } + + // ============ 启动服务端 ============ + @SuppressLint("DiscouragedApi") + public void start() throws IOException { + if (running) return; + running = true; + + serverSocket = new ServerSocket(port); + serverSocket.setSoTimeout(2000); + + acceptExecutor.execute(this::acceptLoop); + scheduler.scheduleAtFixedRate(this::heartbeatCheck, + HEARTBEAT_TIMEOUT_MS, + HEARTBEAT_TIMEOUT_MS, + TimeUnit.MILLISECONDS); + + System.out.println("LanCommunicationManager started on port " + port); + } + + private void acceptLoop() { + while (running) { + try { + Socket socket = serverSocket.accept(); + socket.setSoTimeout((int) HEARTBEAT_TIMEOUT_MS * 2); + + ClientSession session = new ClientSession(socket); + unAuthSessions.put(socket, session); + + clientExecutor.execute(() -> clientReadLoop(session)); + + } catch (SocketTimeoutException ignore) { + } catch (Exception e) { + if (running) e.printStackTrace(); + } + } + } + + // ============ 处理客户端数据读取 ============ + private void clientReadLoop(ClientSession session) { + Socket socket = session.socket; + + try (DataInputStream in = new DataInputStream(socket.getInputStream())) { + while (running && !socket.isClosed()) { + + int len; + try { + len = in.readInt(); + } catch (SocketTimeoutException ste) { + continue; + } + + if (len <= 0 || len > 10 * 1024 * 1024) break; + + byte[] buf = new byte[len]; + in.readFully(buf); + session.updateLastSeen(); + + JSONObject msg = new JSONObject(new String(buf)); + handleMessage(session, msg); + } + } catch (Exception ignored) { + } finally { + closeSession(session); + } + } + + private void handleMessage(ClientSession session, JSONObject msg) { + String type = msg.optString("type", ""); + + switch (type) { + case "auth": + handleAuth(session, msg); + break; + case "heartbeat": + session.updateLastSeen(); + break; + default: + if (listener != null && session.clientId != null) { + listener.onMessageReceived(session.clientId, msg); + } + break; + } + } + + private void handleAuth(ClientSession session, JSONObject msg) { + String clientId = msg.optString("clientId", null); + if (clientId == null) return; + + session.clientId = clientId; + + // 移动到已认证 map + unAuthSessions.remove(session.socket); + clients.put(clientId, session); + + if (listener != null) listener.onClientConnected(clientId); + + sendToSession(session, ack("auth_ok")); + } + + // ============ 心跳超时 ============ + private void heartbeatCheck() { + long now = System.currentTimeMillis(); + + for (Map.Entry e : clients.entrySet()) { + ClientSession s = e.getValue(); + if (now - s.lastSeen > HEARTBEAT_TIMEOUT_MS) { + closeSession(s); + } + } + + for (ClientSession s : unAuthSessions.values()) { + if (now - s.lastSeen > HEARTBEAT_TIMEOUT_MS * 2) { + closeSession(s); + } + } + } + + // ============ 发送 ============ + public boolean sendToClient(String clientId, JSONObject json) { + ClientSession s = clients.get(clientId); + return s != null && sendToSession(s, json); + } + + public void broadcast(JSONObject json) { + for (ClientSession s : clients.values()) { + sendToSession(s, json); + } + } + + private boolean sendToSession(ClientSession s, JSONObject json) { + try { + DataOutputStream out = s.out; + + synchronized (out) { + byte[] data = json.toString().getBytes(); + out.writeInt(data.length); + out.write(data); + out.flush(); + } + return true; + + } catch (Exception e) { + closeSession(s); + return false; + } + } + + // ============ ACK ============ + private JSONObject ack(String type) { + JSONObject j = new JSONObject(); + try { + j.put("type", "ack"); + j.put("ack", type); + } catch (JSONException e) { + throw new RuntimeException(e); + } + + return j; + } + + // ============ 停止 ============ + public void stop() { + running = false; + + try { + serverSocket.close(); + } catch (Exception ignored) { + } + + for (ClientSession s : clients.values()) closeSession(s); + for (ClientSession s : unAuthSessions.values()) closeSession(s); + + acceptExecutor.shutdownNow(); + clientExecutor.shutdownNow(); + scheduler.shutdownNow(); + + System.out.println("LanCommunicationManager stopped"); + } + + // ============ 会话类 ============ + public static class ClientSession { + public final Socket socket; + public final DataOutputStream out; + public volatile long lastSeen = System.currentTimeMillis(); + public volatile String clientId; + + public ClientSession(Socket socket) throws IOException { + this.socket = socket; + this.out = new DataOutputStream(socket.getOutputStream()); + } + + public void updateLastSeen() { + lastSeen = System.currentTimeMillis(); + } + } + + public void closeSession(ClientSession session) { + if (session == null) return; + + try { + Socket socket = session.socket; + + // 1. 从已认证表移除 + if (session.clientId != null) { + ClientSession removed = clients.remove(session.clientId); + if (removed != null && listener != null) { + listener.onClientDisconnected(session.clientId); + } + } + + // 2. 从未认证表移除 + unAuthSessions.remove(socket); + + // 3. 关闭输出流 + try { + session.out.close(); + } catch (Exception ignored) { + } + + // 4. 关闭 socket + try { + if (!socket.isClosed()) socket.close(); + } catch (Exception ignored) { + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/app/src/main/java/com/sw/face/collect/socket/LanServer.java b/app/src/main/java/com/sw/face/collect/socket/LanServer.java new file mode 100644 index 0000000..8d57e1e --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/socket/LanServer.java @@ -0,0 +1,21 @@ +package com.sw.face.collect.socket; + +public class LanServer { + + private static volatile LanCommunicationManager instance; + + public static LanCommunicationManager getInstance() { + if (instance == null) { + synchronized (LanServer.class) { + if (instance == null) { + instance = new LanCommunicationManager( + 5000, // 监听端口 + 20, // 最大客户端数 + 10_000 // 心跳超时时间 10 秒 + ); + } + } + } + return instance; + } +} diff --git a/app/src/main/java/com/sw/face/collect/socket/TcpClient.java b/app/src/main/java/com/sw/face/collect/socket/TcpClient.java new file mode 100644 index 0000000..347906d --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/socket/TcpClient.java @@ -0,0 +1,330 @@ +package com.sw.face.collect.socket; + +import android.util.Log; + +import org.json.JSONObject; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * TcpClient - Android ready + *

+ * Features: + * - length-prefix protocol (int length + bytes) + * - separate read thread and write queue & writer thread + * - auto-reconnect with exponential backoff + * - heartbeat scheduler + * - send queue with optional callback for send result + * - auto send "auth" JSON after connection + */ +public class TcpClient { + + private static final String TAG = "TcpClient"; + + // configuration + private final String serverIp; + private final int serverPort; + private final String clientId; // will be sent in auth message + private final int connectTimeoutMs; + private final long heartbeatIntervalMs; + private final long heartbeatTimeoutMs; + + // socket + streams + private Socket socket; + private DataOutputStream out; + private DataInputStream in; + + // threads & executors + private final ExecutorService writerExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Writer")); + private final ExecutorService readerExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Reader")); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "TcpClient-Scheduler")); + private final ExecutorService connectExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Connect")); + + // send queue + private final BlockingQueue sendQueue = new LinkedBlockingQueue<>(); + + // state + private final AtomicBoolean running = new AtomicBoolean(false); + private final AtomicBoolean connected = new AtomicBoolean(false); + private final AtomicBoolean authSent = new AtomicBoolean(false); + + // reconnection/backoff + private final long baseReconnectDelayMs = 1000; // 1s + private final long maxReconnectDelayMs = 30_000; // 30s + private final AtomicInteger reconnectAttempt = new AtomicInteger(0); + + // heartbeat task future + private ScheduledFuture heartbeatFuture; + + // listener + public interface Listener { + void onConnected(); + + void onDisconnected(Exception e); + + void onMessage(JSONObject json); + + void onSendSuccess(JSONObject json); + + void onSendFailed(JSONObject json, Exception e); + } + + private Listener listener; + + public void setListener(Listener l) { + this.listener = l; + } + + // ctor + public TcpClient(String serverIp, int serverPort, String clientId, + int connectTimeoutMs, long heartbeatIntervalMs, long heartbeatTimeoutMs) { + this.serverIp = serverIp; + this.serverPort = serverPort; + this.clientId = clientId; + this.connectTimeoutMs = connectTimeoutMs; + this.heartbeatIntervalMs = heartbeatIntervalMs; + this.heartbeatTimeoutMs = heartbeatTimeoutMs; + } + + // start client (will attempt connect) + public void start() { + if (running.getAndSet(true)) return; + scheduleConnect(0); + // writer thread drains sendQueue + writerExecutor.execute(this::writerLoop); + } + + // stop client and cleanup + public void stop() { + running.set(false); + cancelHeartbeat(); + closeSocketQuiet(); + writerExecutor.shutdownNow(); + readerExecutor.shutdownNow(); + scheduler.shutdownNow(); + connectExecutor.shutdownNow(); + sendQueue.clear(); + } + + // send JSON (queued). Non-blocking. + public void send(JSONObject json) { + if (!running.get()) return; + sendQueue.offer(json); + } + + // AUTH shortcut (immediately send auth JSON) + private void sendAuth() { + try { + JSONObject auth = new JSONObject(); + auth.put("type", "auth"); + auth.put("clientId", clientId); + sendQueue.offer(auth); + authSent.set(true); + } catch (Exception ignored) { + } + } + + // writer thread loop (serializes sends) + private void writerLoop() { + while (running.get()) { + try { + JSONObject json = sendQueue.take(); // blocks + if (connected.get() && out != null) { + try { + byte[] data = json.toString().getBytes(); + synchronized (out) { + out.writeInt(data.length); + out.write(data); + out.flush(); + } + if (listener != null) listener.onSendSuccess(json); + } catch (Exception e) { + if (listener != null) listener.onSendFailed(json, e); + // on write failure, attempt reconnect + safeCloseAndScheduleReconnect(e); + } + } else { + // not connected: requeue it and wait for connection + sendQueue.offer(json); + Thread.sleep(500); // avoid busy loop + } + } catch (InterruptedException ignored) { + break; + } + } + } + + // reader loop (runs in readerExecutor) + private void startReaderLoop() { + readerExecutor.execute(() -> { + try { + while (running.get() && connected.get() && in != null) { + int length; + try { + length = in.readInt(); // will throw SocketTimeoutException if set + } catch (SocketTimeoutException ste) { + // used to detect socket liveness; continue loop + continue; + } + if (length <= 0 || length > 10 * 1024 * 1024) { + // invalid length, break + throw new RuntimeException("Invalid message length: " + length); + } + byte[] buf = new byte[length]; + in.readFully(buf); + String s = new String(buf); + try { + JSONObject json = new JSONObject(s); + // update last seen time via heartbeat ack if needed + if ("heartbeat".equals(json.optString("type"))) { + // optionally respond or update time + } else if ("auth_ok".equals(json.optString("type")) || "ack".equals(json.optString("type"))) { + // ignore or process ack + } else { + if (listener != null) listener.onMessage(json); + } + } catch (Exception je) { + Log.w(TAG, "Invalid JSON from server: " + s, je); + } + } + } catch (Exception e) { + if (running.get()) { + safeCloseAndScheduleReconnect(e); + } + } + }); + } + + // schedule connect attempt with delay (ms) + private void scheduleConnect(long delayMs) { + connectExecutor.execute(() -> { + try { + if (delayMs > 0) Thread.sleep(delayMs); + } catch (InterruptedException ignored) { + } + if (!running.get()) return; + tryConnect(); + }); + } + + // connect logic + private void tryConnect() { + if (!running.get()) return; + closeSocketQuiet(); // ensure closed + try { + Socket s = new Socket(); + s.connect(new InetSocketAddress(serverIp, serverPort), connectTimeoutMs); + s.setSoTimeout((int) Math.max(heartbeatTimeoutMs, 5_000)); + socket = s; + out = new DataOutputStream(socket.getOutputStream()); + in = new DataInputStream(socket.getInputStream()); + connected.set(true); + reconnectAttempt.set(0); + authSent.set(false); + // start reader + startReaderLoop(); + // send auth immediately + sendAuth(); + // start heartbeat + startHeartbeat(); + if (listener != null) listener.onConnected(); + Log.i(TAG, "Connected to " + serverIp + ":" + serverPort); + } catch (Exception e) { + Log.w(TAG, "Connect failed: " + e.getMessage()); + scheduleReconnectWithBackoff(); + } + } + + // start heartbeat scheduler + private void startHeartbeat() { + cancelHeartbeat(); + heartbeatFuture = scheduler.scheduleAtFixedRate(() -> { + if (!running.get() || !connected.get()) return; + try { + JSONObject hb = new JSONObject(); + hb.put("type", "heartbeat"); + hb.put("time", System.currentTimeMillis()); + sendQueue.offer(hb); + } catch (Exception ignored) { + } + }, 0, heartbeatIntervalMs, TimeUnit.MILLISECONDS); + } + + private void cancelHeartbeat() { + if (heartbeatFuture != null && !heartbeatFuture.isCancelled()) { + heartbeatFuture.cancel(true); + heartbeatFuture = null; + } + } + + // close socket quietly and notify listener + private void safeCloseAndScheduleReconnect(Exception cause) { + closeSocketQuiet(); + if (listener != null) listener.onDisconnected(cause); + scheduleReconnectWithBackoff(); + } + + private void scheduleReconnectWithBackoff() { + int attempt = reconnectAttempt.incrementAndGet(); + long delay = Math.min(maxReconnectDelayMsFromAttempt(attempt), maxReconnectDelayMs); + Log.i(TAG, "Scheduling reconnect attempt " + attempt + " after " + delay + "ms"); + scheduleConnect(delay); + } + + // compute exponential backoff + private long maxReconnectDelayMsFromAttempt(int attempt) { + long d = baseReconnectDelayMs * (1L << Math.min(attempt, 30)); + if (d < 0) d = maxReconnectDelayMs; + return Math.min(d, maxReconnectDelayMs); + } + + // close socket and streams + private void closeSocketQuiet() { + connected.set(false); + cancelHeartbeat(); + try { + if (out != null) { + out.close(); + } + } catch (Exception ignored) { + } + try { + if (in != null) { + in.close(); + } + } catch (Exception ignored) { + } + try { + if (socket != null && !socket.isClosed()) { + socket.close(); + } + } catch (Exception ignored) { + } + out = null; + in = null; + socket = null; + } + + // helper: when manually call reconnect (immediately) + public void reconnectNow() { + scheduleConnect(0); + } + + // helper to set immediate send of a JSON and wait (blocking) until it is queued (not until delivered) + public boolean sendBlocking(JSONObject json, long timeoutMs) throws InterruptedException { + return sendQueue.offer(json, timeoutMs, TimeUnit.MILLISECONDS); + } +} diff --git a/app/src/main/java/com/sw/face/collect/socket/UdpDiscoveryClient.java b/app/src/main/java/com/sw/face/collect/socket/UdpDiscoveryClient.java new file mode 100644 index 0000000..982c1d3 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/socket/UdpDiscoveryClient.java @@ -0,0 +1,47 @@ +package com.sw.face.collect.socket; + +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; + +/** + * Simple UDP discovery client: + * Sends "DISCOVER_SERVER" broadcast and waits for first reply "SERVER_FOUND:serverName:ip" + */ +public class UdpDiscoveryClient { + + public interface Listener { + void onFound(String ip, String serverName); + + void onError(Exception e); + } + + public void discover(int discoveryPort, int timeoutMs, Listener listener) { + new Thread(() -> { + try (DatagramSocket socket = new DatagramSocket()) { + socket.setBroadcast(true); + byte[] data = "DISCOVER_SERVER".getBytes(); + DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("255.255.255.255"), discoveryPort); + socket.send(packet); + + socket.setSoTimeout(timeoutMs); + byte[] buf = new byte[512]; + DatagramPacket resp = new DatagramPacket(buf, buf.length); + socket.receive(resp); + + String msg = new String(resp.getData(), 0, resp.getLength()); + if (msg.startsWith("SERVER_FOUND")) { + // format: SERVER_FOUND:serverName:ip + String[] parts = msg.split(":", 3); + if (parts.length >= 3) { + listener.onFound(parts[2], parts[1]); + return; + } + } + listener.onError(new Exception("Invalid response")); + } catch (Exception e) { + listener.onError(e); + } + }).start(); + } +} diff --git a/app/src/main/java/com/sw/face/collect/socket/test.java b/app/src/main/java/com/sw/face/collect/socket/test.java new file mode 100644 index 0000000..be18d9b --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/socket/test.java @@ -0,0 +1,113 @@ +package com.sw.face.collect.socket; + +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; + +public class test { + LanCommunicationManager manager = new LanCommunicationManager( + 9999, // 端口 + 20, // 最大客户端线程 + 30_000 // 心跳超时 30 秒 + ); + + public void test() { + manager.setListener(new LanCommunicationManager.Listener() { + @Override + public void onClientConnected(String clientId) { + System.out.println("新的客户端上线:" + clientId); + } + + @Override + public void onClientDisconnected(String clientId) { + System.out.println("客户端离线:" + clientId); + } + + @Override + public void onMessageReceived(String clientId, JSONObject msg) { + System.out.println("收到 " + clientId + " 的消息:" + msg); + } + }); + +// 启动 + try { + manager.start(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + // 给某个客户端发送 + JSONObject j = new JSONObject(); + try { + j.put("type", "cmd"); + j.put("content", "hello"); + } catch (JSONException e) { + throw new RuntimeException(e); + } + manager.sendToClient("device123", j); + +// 广播 + manager.broadcast(j); + } + + + public void clientTets() { + // 1) discover server (optional) + UdpDiscoveryClient disc = new UdpDiscoveryClient(); + disc.discover(9876, 3000, new UdpDiscoveryClient.Listener() { + @Override + public void onFound(String ip, String serverName) { + startClient(ip); + } + + @Override + public void onError(Exception e) { /* fallback to manual IP */ } + }); + + + } + + // 2) start client + private TcpClient client; + + private void startClient(String serverIp) { + client = new TcpClient( + serverIp, + 9999, + "device123", // clientId + 5000, // connectTimeoutMs + 10_000, // heartbeatIntervalMs + 30_000 // heartbeatTimeoutMs + ); + + client.setListener(new TcpClient.Listener() { + @Override + public void onConnected() { + Log.i("APP", "connected"); + } + + @Override + public void onDisconnected(Exception e) { + Log.i("APP", "disconnected", e); + } + + @Override + public void onMessage(JSONObject json) { + Log.i("APP", "msg:" + json); + } + + @Override + public void onSendSuccess(JSONObject json) { + } + + @Override + public void onSendFailed(JSONObject json, Exception e) { + } + }); + + client.start(); + } +} diff --git a/app/src/main/java/com/sw/face/collect/utils/Base64.java b/app/src/main/java/com/sw/face/collect/utils/Base64.java new file mode 100644 index 0000000..4509ef6 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/utils/Base64.java @@ -0,0 +1,265 @@ +package com.sw.face.collect.utils; + +public final class Base64 { + + private static final int BASELENGTH = 128; + private static final int LOOKUPLENGTH = 64; + private static final int TWENTYFOURBITGROUP = 24; + private static final int EIGHTBIT = 8; + private static final int SIXTEENBIT = 16; + private static final int FOURBYTE = 4; + private static final int SIGN = -128; + private static char PAD = '='; + private static byte[] base64Alphabet = new byte[BASELENGTH]; + private static char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH]; + + static { + for (int i = 0; i < BASELENGTH; ++i) { + base64Alphabet[i] = -1; + } + for (int i = 'Z'; i >= 'A'; i--) { + base64Alphabet[i] = (byte) (i - 'A'); + } + for (int i = 'z'; i >= 'a'; i--) { + base64Alphabet[i] = (byte) (i - 'a' + 26); + } + + for (int i = '9'; i >= '0'; i--) { + base64Alphabet[i] = (byte) (i - '0' + 52); + } + + base64Alphabet['+'] = 62; + base64Alphabet['/'] = 63; + + for (int i = 0; i <= 25; i++) { + lookUpBase64Alphabet[i] = (char) ('A' + i); + } + + for (int i = 26, j = 0; i <= 51; i++, j++) { + lookUpBase64Alphabet[i] = (char) ('a' + j); + } + + for (int i = 52, j = 0; i <= 61; i++, j++) { + lookUpBase64Alphabet[i] = (char) ('0' + j); + } + lookUpBase64Alphabet[62] = (char) '+'; + lookUpBase64Alphabet[63] = (char) '/'; + + } + + private static boolean isWhiteSpace(char octect) { + return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9); + } + + private static boolean isPad(char octect) { + return (octect == PAD); + } + + private static boolean isData(char octect) { + return (octect < BASELENGTH && base64Alphabet[octect] != -1); + } + + /** + * Encodes hex octects into Base64 + * + * @param binaryData Array containing binaryData + * @return Encoded Base64 array + */ + public static String encode(byte[] binaryData) { + + if (binaryData == null) { + return null; + } + + int lengthDataBits = binaryData.length * EIGHTBIT; + if (lengthDataBits == 0) { + return ""; + } + + int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP; + int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP; + int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 + : numberTriplets; + char encodedData[] = null; + + encodedData = new char[numberQuartet * 4]; + + byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0; + + int encodedIndex = 0; + int dataIndex = 0; + + for (int i = 0; i < numberTriplets; i++) { + b1 = binaryData[dataIndex++]; + b2 = binaryData[dataIndex++]; + b3 = binaryData[dataIndex++]; + + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) + : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) + : (byte) ((b2) >> 4 ^ 0xf0); + byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) + : (byte) ((b3) >> 6 ^ 0xfc); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f]; + } + + // form integral number of 6-bit groups + if (fewerThan24bits == EIGHTBIT) { + b1 = binaryData[dataIndex]; + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) + : (byte) ((b1) >> 2 ^ 0xc0); + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4]; + encodedData[encodedIndex++] = PAD; + encodedData[encodedIndex++] = PAD; + } else if (fewerThan24bits == SIXTEENBIT) { + b1 = binaryData[dataIndex]; + b2 = binaryData[dataIndex + 1]; + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) + : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) + : (byte) ((b2) >> 4 ^ 0xf0); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2]; + encodedData[encodedIndex++] = PAD; + } + + return new String(encodedData); + } + + /** + * Decodes Base64 data into octects + * + * @param encoded string containing Base64 data + * @return Array containind decoded data. + */ + public static byte[] decode(String encoded) { + + if (encoded == null) { + return null; + } + + char[] base64Data = encoded.toCharArray(); + // remove white spaces + int len = removeWhiteSpace(base64Data); + + if (len % FOURBYTE != 0) { + return null;// should be divisible by four + } + + int numberQuadruple = (len / FOURBYTE); + + if (numberQuadruple == 0) { + return new byte[0]; + } + + byte decodedData[] = null; + byte b1 = 0, b2 = 0, b3 = 0, b4 = 0; + char d1 = 0, d2 = 0, d3 = 0, d4 = 0; + + int i = 0; + int encodedIndex = 0; + int dataIndex = 0; + decodedData = new byte[(numberQuadruple) * 3]; + + for (; i < numberQuadruple - 1; i++) { + + if (!isData((d1 = base64Data[dataIndex++])) + || !isData((d2 = base64Data[dataIndex++])) + || !isData((d3 = base64Data[dataIndex++])) + || !isData((d4 = base64Data[dataIndex++]))) { + return null; + }// if found "no data" just return null + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + } + + if (!isData((d1 = base64Data[dataIndex++])) + || !isData((d2 = base64Data[dataIndex++]))) { + return null;// if found "no data" just return null + } + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + + d3 = base64Data[dataIndex++]; + d4 = base64Data[dataIndex++]; + if (!isData((d3)) || !isData((d4))) {// Check if they are PAD characters + if (isPad(d3) && isPad(d4)) { + if ((b2 & 0xf) != 0)// last 4 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 1]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + return tmp; + } else if (!isPad(d3) && isPad(d4)) { + b3 = base64Alphabet[d3]; + if ((b3 & 0x3) != 0)// last 2 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 2]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + return tmp; + } else { + return null; + } + } else { // No PAD e.g 3cQl + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + + } + + return decodedData; + } + + /** + * remove WhiteSpace from MIME containing encoded Base64 data. + * + * @param data the byte array of base64 data (with WS) + * @return the new length + */ + private static int removeWhiteSpace(char[] data) { + if (data == null) { + return 0; + } + + // count characters that's not whitespace + int newSize = 0; + int len = data.length; + for (int i = 0; i < len; i++) { + if (!isWhiteSpace(data[i])) { + data[newSize++] = data[i]; + } + } + return newSize; + } +} diff --git a/app/src/main/java/com/sw/face/collect/utils/BitmapFlipUtil.java b/app/src/main/java/com/sw/face/collect/utils/BitmapFlipUtil.java new file mode 100644 index 0000000..c51842f --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/utils/BitmapFlipUtil.java @@ -0,0 +1,39 @@ +package com.sw.face.collect.utils; + +import android.graphics.Bitmap; +import android.graphics.Matrix; + +public class BitmapFlipUtil { + /** + * 水平翻转Bitmap + * @param src 原始Bitmap + * @return 水平翻转后的Bitmap + */ + public static Bitmap flipHorizontally(Bitmap src) { + Matrix matrix = new Matrix(); + matrix.preScale(-1, 1); // 水平翻转 + return Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, false); + } + + /** + * 垂直翻转Bitmap + * @param src 原始Bitmap + * @return 垂直翻转后的Bitmap + */ + public static Bitmap flipVertically(Bitmap src) { + Matrix matrix = new Matrix(); + matrix.preScale(1, -1); // 垂直翻转 + return Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, false); + } + + /** + * 水平和垂直同时翻转(180°旋转) + * @param src 原始Bitmap + * @return 180°旋转后的Bitmap + */ + public static Bitmap flipBoth(Bitmap src) { + Matrix matrix = new Matrix(); + matrix.preScale(-1, -1); // 同时水平和垂直翻转 + return Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, false); + } +} diff --git a/app/src/main/java/com/sw/face/collect/utils/BitmapToByteArrayUtil.kt b/app/src/main/java/com/sw/face/collect/utils/BitmapToByteArrayUtil.kt new file mode 100644 index 0000000..cd6135f --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/utils/BitmapToByteArrayUtil.kt @@ -0,0 +1,38 @@ +package com.sw.face.collect.utils +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import java.io.ByteArrayOutputStream + +object BitmapToByteArrayUtil { + /** + * Bitmap转PNG格式byte[] + * @param bitmap 原始Bitmap + * @return PNG格式的byte[] + */ + fun bitmapToPngByteArray(bitmap: Bitmap): ByteArray { + val stream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) + return stream.toByteArray() + } + + /** + * Bitmap转JPEG格式byte[] + * @param bitmap 原始Bitmap + * @param quality 压缩质量(0-100) + * @return JPEG格式的byte[] + */ + fun bitmapToJpegByteArray(bitmap: Bitmap, quality: Int): ByteArray { + val stream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream) + return stream.toByteArray() + } + + /** + * byte[]转Bitmap(通用格式) + * @param data byte数组 + * @return 解析后的Bitmap + */ + fun byteArrayToBitmap(data: ByteArray): Bitmap { + return BitmapFactory.decodeByteArray(data, 0, data.size) + } +} diff --git a/app/src/main/java/com/sw/face/collect/utils/BitmapUtils.java b/app/src/main/java/com/sw/face/collect/utils/BitmapUtils.java new file mode 100644 index 0000000..a06207f --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/utils/BitmapUtils.java @@ -0,0 +1,36 @@ +package com.sw.face.collect.utils; + +import android.graphics.Bitmap; +import android.graphics.Color; + +public class BitmapUtils { + + public static boolean isBlackWhite(Bitmap bmp) { + Color c = null; + //遍历图片的像素点 + for (int y = 0; y < 100; y++) { + for (int x = 0; x < 100; x++) { + c = bmp.getColor(x, y); + +// L.e(c.red() + "===" + c.green() + "===" + c.blue()); + //判断像素点的色偏差值Diff + if (getRGBDiff(c.red(), c.green(), c.blue()) != 0) { + return false; + } + } + } + + return true; + } + + private static float getRGBDiff(float r, float g, float b) { + float test1 = Math.abs(r - g); + float test2 = Math.abs(r - b); + float test3 = Math.abs(g - b); + + float max = Math.max(test1, test2); + max = Math.max(max, test3); + return max; + } + +} diff --git a/app/src/main/java/com/sw/face/collect/utils/CameraUtils.kt b/app/src/main/java/com/sw/face/collect/utils/CameraUtils.kt new file mode 100644 index 0000000..8e6b0ce --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/utils/CameraUtils.kt @@ -0,0 +1,86 @@ +package com.sw.face.collect.utils + +import android.net.Uri +import androidx.activity.ComponentActivity +import androidx.camera.core.CameraSelector +import androidx.camera.view.CameraController +import androidx.camera.view.LifecycleCameraController +import androidx.camera.view.PreviewView + +class CameraUtils(private var activity: ComponentActivity) { + + private var cameraController: LifecycleCameraController? = null + private var photoCaptureHelper: PhotoCaptureHelper? = null + private var failCallback: ((msg: String) -> Unit)? = null + + // private var isCameraReady = false + fun takePhoto(succCallback: (Uri) -> Unit, failCallback: (msg: String) -> Unit = { }) { + this.failCallback = failCallback + cameraController?.let { + if (photoCaptureHelper == null) { + initCaptureHelper() + } + } + photoCaptureHelper?.let { + it.addSuccessCallback(succCallback) + it.bindCameraCallback { + bind() + } + it.takePhoto() + } + } + + private fun initCaptureHelper() { + photoCaptureHelper = PhotoCaptureHelper( + context = activity, + cameraController = cameraController!!, + onSuccess = {}, + onError = { msg -> + //toast(msg) + failCallback?.invoke(msg) + } + ) + } + + fun setPreviewController(previewView: PreviewView?) { + if (previewView?.controller == null) { + previewView?.controller = cameraController + } + } + + fun initCamera() { + if (cameraController == null) { + cameraController = LifecycleCameraController(activity).apply { + // 必须设置有效的用例 + setEnabledUseCases( + CameraController.IMAGE_CAPTURE + // or CameraController.VIDEO_CAPTURE + ) + cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA +// cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA +// setWhiteBalanceMode(WhiteBalanceMode.AUTO) + } + bind() + } + +// if (isCameraReady.not()) { +// try { +// cameraController!!.initializationFuture.addListener({ +// isCameraReady = true +// Timber.d("Camera initialized successfully") +// }, ContextCompat.getMainExecutor(this)) +// } catch (e: Exception) { +// Timber.d("Camera initialized error = ${e.message}") +// } +// } + } + + fun bind() { + cameraController?.bindToLifecycle(activity) + } + + fun unbind() { + cameraController?.unbind() + } + +} diff --git a/app/src/main/java/com/sw/face/collect/utils/Debouncer.kt b/app/src/main/java/com/sw/face/collect/utils/Debouncer.kt new file mode 100644 index 0000000..89984ca --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/utils/Debouncer.kt @@ -0,0 +1,28 @@ +package com.sw.face.collect.utils + +/** + * 防抖工具 + */ +class Debouncer(private val delayMillis: Long = 2000) { + private var lastActionTime = 0L + + /** + * 执行防抖操作 + * @param action 要执行的操作 + * @return Boolean 是否执行了操作 (true=已执行, false=被防抖) + */ + fun debounce(action: () -> Unit): Boolean { + val currentTime = System.currentTimeMillis() + if (currentTime - lastActionTime >= delayMillis) { + lastActionTime = currentTime + action() + return true + } + return false + } + + // 重置防抖计时 + fun reset() { + lastActionTime = 0L + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/utils/FaceEngineUtils.kt b/app/src/main/java/com/sw/face/collect/utils/FaceEngineUtils.kt new file mode 100644 index 0000000..a16ba48 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/utils/FaceEngineUtils.kt @@ -0,0 +1,47 @@ +package com.sw.face.collect.utils + +import android.util.Log +import com.arcsoft.face.ErrorInfo +import com.sw.face.collect.base.GlobalData +import com.sw.plate.App +import com.sw.plate.utils.ToastUtils +import com.sw.plate.utils.arcface.FaceApi + +object FaceEngineUtils { + + private val faceApi: FaceApi by lazy { FaceApi() } + + /** + * 激活人脸识别引擎 + */ + fun activeEngine() { + faceApi.activeEngine( + App.getContext(), + GlobalData.appId, + GlobalData.sdkKey, + GlobalData.activeKey, + object : FaceApi.ActiveCallback { + override fun onSuccess(activeCode: Int) { + Log.d("FaceEngineUtils", "activeEngine activeCode = $activeCode") + when (activeCode) { + ErrorInfo.MOK -> { + ToastUtils.showToast("激活引擎成功") + } + + ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> { +// ToastUtils.showToast("引擎已激活,无需再次激活") + } + + else -> { + ToastUtils.showToast("激活引擎失败($activeCode)") + } + } + } + + override fun onFail(e: Exception?) { + ToastUtils.showToast("激活引擎异常,${e?.message}") + } + }) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/utils/FlowCountDown.kt b/app/src/main/java/com/sw/face/collect/utils/FlowCountDown.kt new file mode 100644 index 0000000..57f9d44 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/utils/FlowCountDown.kt @@ -0,0 +1,33 @@ +package com.sw.face.collect.utils + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart + +// 倒计时Flow扩展函数 +fun countDownByFlow( + total: Int, + scope: CoroutineScope, + onTick: (Int) -> Unit, + onStart: (() -> Unit)? = null, + onFinish: (() -> Unit)? = null +): Job { + return flow { + for (i in total downTo 0) { + emit(i) + if (i != 0) delay(1000) + } + }.flowOn(Dispatchers.Main) + .onStart { onStart?.invoke() } + .onCompletion { onFinish?.invoke() } + .onEach { onTick.invoke(it) } + .launchIn(scope) +} diff --git a/app/src/main/java/com/sw/face/collect/utils/IntervalExecutor.kt b/app/src/main/java/com/sw/face/collect/utils/IntervalExecutor.kt new file mode 100644 index 0000000..44a78c8 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/utils/IntervalExecutor.kt @@ -0,0 +1,66 @@ +package com.sw.face.collect.utils + +import kotlinx.coroutines.* +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +class IntervalExecutor { + + /** + * 启动定时任务 + * @param delayMillis 延迟时间(毫秒) + * @param action 要执行的方法 + * @return Job 可用于取消任务 + */ + fun startIntervalTask(delayMillis: Long, action: suspend () -> Unit): Job { + return CoroutineScope(Dispatchers.Default).launch { + while (isActive) { + action() + delay(delayMillis) + } + } + } + + /** + * 启动定时任务(带初始延迟) + * @param initialDelay 初始延迟时间(毫秒) + * @param delayMillis 后续执行间隔(毫秒) + * @param action 要执行的方法 + * @return Job 可用于取消任务 + */ + fun startIntervalTaskWithInitialDelay( + initialDelay: Long, + delayMillis: Long, + action: suspend () -> Unit + ): Job { + return CoroutineScope(Dispatchers.Default).launch { + delay(initialDelay) + while (isActive) { + action() + delay(delayMillis) + } + } + } +} + +// 使用示例 +fun main() = runBlocking { + val executor = IntervalExecutor() + + // 示例1:每隔10秒执行一次 + val job1 = executor.startIntervalTask(10000) { + println("定时任务执行: ${System.currentTimeMillis()}") + // 这里可以执行你的业务逻辑 + } + + // 示例2:先延迟5秒,然后每隔3秒执行一次 + val job2 = executor.startIntervalTaskWithInitialDelay(5000, 3000) { + println("带初始延迟的定时任务: ${System.currentTimeMillis()}") + } + + // 运行30秒后取消任务 + delay(30000) + job1.cancel() + job2.cancel() + println("所有定时任务已取消") +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/utils/NetworkUtils.java b/app/src/main/java/com/sw/face/collect/utils/NetworkUtils.java new file mode 100644 index 0000000..e03e0fb --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/utils/NetworkUtils.java @@ -0,0 +1,50 @@ +package com.sw.face.collect.utils; + +import androidx.annotation.RequiresPermission; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Enumeration; +import java.util.LinkedList; + +public class NetworkUtils { + + @RequiresPermission("android.permission.INTERNET") + public static String getIPAddress(boolean useIPv4) { + try { + Enumeration nis = NetworkInterface.getNetworkInterfaces(); + LinkedList adds = new LinkedList(); + + while(nis.hasMoreElements()) { + NetworkInterface ni = (NetworkInterface)nis.nextElement(); + if (ni.isUp() && !ni.isLoopback()) { + Enumeration addresses = ni.getInetAddresses(); + + while(addresses.hasMoreElements()) { + adds.addFirst(addresses.nextElement()); + } + } + } + + for(InetAddress add : adds) { + if (!add.isLoopbackAddress()) { + String hostAddress = add.getHostAddress(); + boolean isIPv4 = hostAddress.indexOf(58) < 0; + if (useIPv4) { + if (isIPv4) { + return hostAddress; + } + } else if (!isIPv4) { + int index = hostAddress.indexOf(37); + return index < 0 ? hostAddress.toUpperCase() : hostAddress.substring(0, index).toUpperCase(); + } + } + } + } catch (SocketException e) { + e.printStackTrace(); + } + + return ""; + } +} diff --git a/app/src/main/java/com/sw/face/collect/utils/PhotoCaptureHelper.kt b/app/src/main/java/com/sw/face/collect/utils/PhotoCaptureHelper.kt new file mode 100644 index 0000000..51b63a4 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/utils/PhotoCaptureHelper.kt @@ -0,0 +1,97 @@ +package com.sw.face.collect.utils + +import android.content.Context +import android.net.Uri +import android.util.Log +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageCaptureException +import androidx.camera.view.CameraController +import androidx.core.content.ContextCompat +import java.io.File + +/** + * 拍照工具类 + * @param context Context 上下文 + * @param cameraController CameraController 相机控制器 + * @param onSuccess (Uri) -> Unit 拍照成功回调 + * @param onError (String) -> Unit 拍照失败回调 + */ +class PhotoCaptureHelper( + private val context: Context, + private val cameraController: CameraController, + private val onSuccess: (Uri) -> Unit = {}, + private val onError: (String) -> Unit = {} +) { + + companion object { + const val TAG = "PhotoCaptureHelper" + } + + private val callbackList: MutableList<(Uri) -> Unit> = mutableListOf() + fun addSuccessCallback(callback: (Uri) -> Unit) { + if (callbackList.contains(callback).not()) { + callbackList.add(callback) + } + } + + private var bindCamera: (() -> Unit)? = null + fun bindCameraCallback(callback: () -> Unit) { + this.bindCamera = callback + } + + /** + * 拍照方法 + * @param fileNamePrefix 文件名前缀,默认为"IMG_" + * @param fileExtension 文件扩展名,默认为".jpg" + */ + fun takePhoto( + fileNamePrefix: String = "IMG_", + fileExtension: String = ".jpg" + ) { + Log.d(TAG, "开始拍照采集") + + try { + val executor = ContextCompat.getMainExecutor(context) + val cacheDir = context.cacheDir + val photoFile = File.createTempFile( + "${fileNamePrefix}${System.currentTimeMillis()}", + fileExtension, + cacheDir + ) + + val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() + + cameraController.takePicture( + outputOptions, + executor, + object : ImageCapture.OnImageSavedCallback { + override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { + val photoUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile) + Log.d(TAG, "照片保存成功: $photoUri") + onSuccess(photoUri) + callbackList.forEach { + it(photoUri) + } + } + + override fun onError(exception: ImageCaptureException) { + val errorMsg = "拍照失败: ${exception.message}" + if (errorMsg.contains("Not bound to a valid Camera")) { + if (bindCamera != null) { + bindCamera?.invoke() + //takePhoto() + } + + } + Log.e(TAG, errorMsg) + onError(errorMsg) + } + } + ) + } catch (e: Exception) { + val errorMsg = "创建临时文件失败: ${e.message}" + Log.e(TAG, errorMsg) + onError(errorMsg) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/utils/ToastUtils.java b/app/src/main/java/com/sw/face/collect/utils/ToastUtils.java new file mode 100644 index 0000000..160ee51 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/utils/ToastUtils.java @@ -0,0 +1,117 @@ +package com.sw.face.collect.utils; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import com.sw.face.collect.MyApp; +import com.sw.face.collect.R; + + +/** + * The type Toast utils. + */ +public class ToastUtils { + + private static Toast toast; + @SuppressLint("StaticFieldLeak") + private static TextView textCenterView; + + /** + * Show center toast. + * + * @param text the text + */ + public static void showToast(String text, int duration) { + if (TextUtils.isEmpty(text)) { + return; + } + runOnMainThread(()->{ + Context context = MyApp.Companion.getInstance(); + if (toast == null) { + View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null); + textCenterView = view.findViewById(R.id.toast_tv); + toast = new Toast(context); + toast.setGravity(Gravity.CENTER, 0, 20); + toast.setDuration(duration); + toast.setView(view); + } + textCenterView.setText(text); + toast.show(); + }); + } + + public static void runOnMainThread(Runnable runnable) { + new Handler(Looper.getMainLooper()).post(runnable); + } + + /** + * Show center toast. + * + * @param text the text + */ + public static void showToast(String text) { + showToast(text, Toast.LENGTH_SHORT); + +// if (snackbar == null) { +// View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null); +// textCenterView = view.findViewById(R.id.toast_tv); +// snackbar = Snackbar.make(view, "", Snackbar.LENGTH_SHORT); +// +// } +// textCenterView.setText(text); +// snackbar.setDuration(Snackbar.LENGTH_SHORT); +// snackbar.show(); + } + +// private static Snackbar snackbar = null; + +// +// public static void showLongToast(String text) { +// runOnMainThread(()-> { +// Context context = App.Companion.getInstance(); +// if (toast == null) { +// View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null); +// textCenterView = view.findViewById(R.id.toast_tv); +// toast = new Toast(context); +// toast.setGravity(Gravity.TOP, 0, 20); +// toast.setDuration(Toast.LENGTH_LONG); +// toast.setView(view); +// } +// textCenterView.setText(text); +// showMyToast(toast, 1000000 * 30); +// }); +// } +// +// +// //自定义Toast控件 +// private static void showMyToast(final Toast toast, final int cnt) { +// final Timer timer = new Timer(); +// timer.schedule(new TimerTask() { +// @Override +// public void run() { +// toast.show(); +// } +// }, 0, Toast.LENGTH_LONG); +// new Timer().schedule(new TimerTask() { +// @Override +// public void run() { +// toast.cancel(); +// timer.cancel(); +// } +// }, cnt); +// } +// +// public static void showSystemToast(String text) { +// Context context = App.Companion.getInstance(); +// Toast.makeText(context, text, Toast.LENGTH_SHORT).show(); +// } + +} diff --git a/app/src/main/java/com/sw/face/collect/view/CustomDialog.java b/app/src/main/java/com/sw/face/collect/view/CustomDialog.java new file mode 100644 index 0000000..a79a7d8 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/view/CustomDialog.java @@ -0,0 +1,101 @@ +package com.sw.face.collect.view; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.view.Gravity; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +import com.sw.face.collect.R; + +/** + * 加载进度条弹窗 + */ +public class CustomDialog extends Dialog { + + /** + * 宽高由布局文件中指定(但是最底层的宽度无效,可以多嵌套一层解决) + */ + public CustomDialog(Context context, View layout) { + super(context, R.style.LoadingDialog); + setContentView(layout); + initConfig(); + } + + public CustomDialog(Context context, View layout, int style) { + super(context, style); + setContentView(layout); + initConfig(); + } + + private void initConfig() { + setCancelable(true); + setCanceledOnTouchOutside(true); + Window window = getWindow(); + if (window != null) { + window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + WindowManager.LayoutParams params = window.getAttributes(); + params.gravity = Gravity.CENTER; + params.width = WindowManager.LayoutParams.WRAP_CONTENT; + params.height = WindowManager.LayoutParams.WRAP_CONTENT; + window.setAttributes(params); + } + } +// +// /** +// * 宽高由该方法的参数设置 +// */ +// public CustomDialog(Context context, int width, int height, View layout, +// int style) { +// super(context, style); +// // 设置内容 +// setContentView(layout); +// // 设置窗口属性 +// Window window = getWindow(); +// WindowManager.LayoutParams params = window.getAttributes(); +// // 设置宽度、高度、密度、对齐方式 +// float density = getDensity(context); +// params.width = (int) (width * density); +// params.height = (int) (height * density); +// params.gravity = Gravity.CENTER; +// window.setAttributes(params); +// +// } +// +// /** +// * 获取显示密度 +// * +// * @param context +// * @return +// */ +// public float getDensity(Context context) { +// Resources res = context.getResources(); +// DisplayMetrics dm = res.getDisplayMetrics(); +// return dm.density; +// } +// +// private void fullScreenImmersive(View view) { +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { +// int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE +// | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION +// | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION +// | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY +// | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN +// | View.SYSTEM_UI_FLAG_FULLSCREEN; +// view.setSystemUiVisibility(uiOptions); +// } +// +// } + +// @Override +// public void show() { +// this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); +// super.show(); +//// fullScreenImmersive(getWindow().getDecorView()); +//// this.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); +// } + +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/view/LanServerListenerImpl.kt b/app/src/main/java/com/sw/face/collect/view/LanServerListenerImpl.kt new file mode 100644 index 0000000..b910c07 --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/view/LanServerListenerImpl.kt @@ -0,0 +1,22 @@ +package com.sw.face.collect.view + +import android.util.Log +import com.sw.face.collect.socket.LanCommunicationManager +import org.json.JSONObject + +open class LanServerListenerImpl: LanCommunicationManager.Listener { + companion object { + private const val TAG = "LanServerListenerImpl" + } + override fun onClientConnected(clientId: String?) { + Log.d(TAG, "addSocketListener,onClientConnected: clientId = $clientId") + } + + override fun onClientDisconnected(clientId: String?) { + Log.d(TAG, "addSocketListener,onClientDisconnected: clientId = $clientId") + } + + override fun onMessageReceived(clientId: String?, message: JSONObject?) { + Log.d(TAG, "addSocketListener,onMessageReceived: clientId = $clientId,message = $message") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/view/TcpClientListenerImpl.kt b/app/src/main/java/com/sw/face/collect/view/TcpClientListenerImpl.kt new file mode 100644 index 0000000..82b4e2a --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/view/TcpClientListenerImpl.kt @@ -0,0 +1,34 @@ +package com.sw.face.collect.view + +import android.util.Log +import com.sw.face.collect.socket.TcpClient +import org.json.JSONObject +import java.lang.Exception +import kotlin.math.log + +open class TcpClientListenerImpl : TcpClient.Listener { + + companion object { + private const val TAG = "TcpClientListenerImpl" + } + + override fun onConnected() { + Log.d(TAG, "addSocketListener,onConnected: ") + } + + override fun onDisconnected(e: Exception?) { + Log.d(TAG, "addSocketListener,onDisconnected: ${e?.toString()}") + } + + override fun onMessage(json: JSONObject?) { + Log.d(TAG, "addSocketListener,onMessage: $json") + } + + override fun onSendSuccess(json: JSONObject?) { + Log.d(TAG, "addSocketListener,onSendSuccess: $json") + } + + override fun onSendFailed(json: JSONObject?, e: Exception?) { + Log.d(TAG, "addSocketListener,onSendFailed: $json,e:${e?.toString()}") + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-xxhdpi/bg_collect_btn.png b/app/src/main/res/drawable-xxhdpi/bg_collect_btn.png new file mode 100644 index 0000000..e8621c0 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/bg_collect_btn.png differ diff --git a/app/src/main/res/drawable-xxhdpi/bg_collect_page.png b/app/src/main/res/drawable-xxhdpi/bg_collect_page.png new file mode 100644 index 0000000..17c061c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/bg_collect_page.png differ diff --git a/app/src/main/res/drawable-xxhdpi/bg_collect_page2.png b/app/src/main/res/drawable-xxhdpi/bg_collect_page2.png new file mode 100644 index 0000000..f5fd8ea Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/bg_collect_page2.png differ diff --git a/app/src/main/res/drawable-xxhdpi/bg_face_wrap.png b/app/src/main/res/drawable-xxhdpi/bg_face_wrap.png new file mode 100644 index 0000000..af847c5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/bg_face_wrap.png differ diff --git a/app/src/main/res/drawable-xxhdpi/bg_face_wrap2.png b/app/src/main/res/drawable-xxhdpi/bg_face_wrap2.png new file mode 100644 index 0000000..2fa2e00 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/bg_face_wrap2.png differ diff --git a/app/src/main/res/drawable-xxhdpi/face_mask.png b/app/src/main/res/drawable-xxhdpi/face_mask.png new file mode 100644 index 0000000..6cb6f06 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/face_mask.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_camera256.png b/app/src/main/res/drawable-xxhdpi/ic_camera256.png new file mode 100644 index 0000000..cc72a98 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_camera256.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_camera_rect.png b/app/src/main/res/drawable-xxhdpi/ic_camera_rect.png new file mode 100644 index 0000000..0e5dcdb Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_camera_rect.png differ diff --git a/app/src/main/res/drawable-xxhdpi/img_init.png b/app/src/main/res/drawable-xxhdpi/img_init.png new file mode 100644 index 0000000..e8d0844 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/img_init.png differ diff --git a/app/src/main/res/drawable/bg_collect_shape.xml b/app/src/main/res/drawable/bg_collect_shape.xml new file mode 100644 index 0000000..a12686e --- /dev/null +++ b/app/src/main/res/drawable/bg_collect_shape.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_splash.xml b/app/src/main/res/drawable/bg_splash.xml new file mode 100644 index 0000000..56c76f4 --- /dev/null +++ b/app/src/main/res/drawable/bg_splash.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_take_photo.xml b/app/src/main/res/drawable/bg_take_photo.xml new file mode 100644 index 0000000..0a69861 --- /dev/null +++ b/app/src/main/res/drawable/bg_take_photo.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_top_mask.xml b/app/src/main/res/drawable/bg_top_mask.xml new file mode 100644 index 0000000..ab2fac0 --- /dev/null +++ b/app/src/main/res/drawable/bg_top_mask.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dialog_waiting.xml b/app/src/main/res/drawable/dialog_waiting.xml new file mode 100644 index 0000000..e7baf2a --- /dev/null +++ b/app/src/main/res/drawable/dialog_waiting.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..66cf345 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main_bak.xml b/app/src/main/res/layout/activity_main_bak.xml new file mode 100644 index 0000000..acc1186 --- /dev/null +++ b/app/src/main/res/layout/activity_main_bak.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_waiting.xml b/app/src/main/res/layout/dialog_waiting.xml new file mode 100644 index 0000000..304abc1 --- /dev/null +++ b/app/src/main/res/layout/dialog_waiting.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_camera_preview.xml b/app/src/main/res/layout/layout_camera_preview.xml new file mode 100644 index 0000000..50a2a22 --- /dev/null +++ b/app/src/main/res/layout/layout_camera_preview.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_face_recognize.xml b/app/src/main/res/layout/layout_face_recognize.xml new file mode 100644 index 0000000..89d0efe --- /dev/null +++ b/app/src/main/res/layout/layout_face_recognize.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/toast_bg.xml b/app/src/main/res/layout/toast_bg.xml new file mode 100644 index 0000000..8c87595 --- /dev/null +++ b/app/src/main/res/layout/toast_bg.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..94f36cb --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..35c4719 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #FF000000 + #FFFFFFFF + #FF03DAC5 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..3256434 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + 人脸信息采集 + %s 初始化失败,错误码:%d\n错误码常量名:%s + 权限被拒绝! + \n可能的原因:该设备不支持同时打开两个摄像头 + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..b0963b6 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..4df9255 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..738a53a --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..f443c3c --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..922f551 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,5 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..20e2a01 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lib_face/.gitignore b/lib_face/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/lib_face/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/lib_face/build.gradle.kts b/lib_face/build.gradle.kts new file mode 100644 index 0000000..449f75b --- /dev/null +++ b/lib_face/build.gradle.kts @@ -0,0 +1,72 @@ +plugins { +// alias(libs.plugins.android.library) + id("com.android.library") +} + +android { + namespace = "com.sw.plate" + compileSdk = 35 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + + ndk { + abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/)) + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + sourceSets { + named("main") { + jniLibs.srcDirs("libs") + } + } +} + +dependencies { + api( + fileTree( + mapOf( + "dir" to "libs", + "include" to listOf("*.aar", "*.jar") + ) + ) + ) + implementation(libs.androidx.appcompat) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + +// implementation("com.licheedev:android-serialport:2.1.5") + + val roomVersion = "2.2.5" + implementation("androidx.room:room-runtime:$roomVersion") + annotationProcessor("androidx.room:room-compiler:$roomVersion") + + implementation("io.reactivex.rxjava2:rxandroid:2.0.1") + implementation("com.google.code.gson:gson:2.8.6") + + val glideVersion = "4.12.0" + implementation("com.github.bumptech.glide:glide:$glideVersion") + annotationProcessor("com.github.bumptech.glide:compiler:$glideVersion") + + val objectboxVersion = "5.0.1" + debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion") +// releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion") +} \ No newline at end of file diff --git a/lib_face/consumer-rules.pro b/lib_face/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/lib_face/libs/WbIntelligentHardwareSDK-debug.aar b/lib_face/libs/WbIntelligentHardwareSDK-debug.aar new file mode 100644 index 0000000..c25490d Binary files /dev/null and b/lib_face/libs/WbIntelligentHardwareSDK-debug.aar differ diff --git a/lib_face/libs/arcsoft_face.jar b/lib_face/libs/arcsoft_face.jar new file mode 100644 index 0000000..b2e827c Binary files /dev/null and b/lib_face/libs/arcsoft_face.jar differ diff --git a/lib_face/libs/arcsoft_image_util.jar b/lib_face/libs/arcsoft_image_util.jar new file mode 100644 index 0000000..2260c13 Binary files /dev/null and b/lib_face/libs/arcsoft_image_util.jar differ diff --git a/lib_face/libs/armeabi-v7a/libarcsoft_face.so b/lib_face/libs/armeabi-v7a/libarcsoft_face.so new file mode 100644 index 0000000..e37f157 Binary files /dev/null and b/lib_face/libs/armeabi-v7a/libarcsoft_face.so differ diff --git a/lib_face/libs/armeabi-v7a/libarcsoft_face_engine.so b/lib_face/libs/armeabi-v7a/libarcsoft_face_engine.so new file mode 100644 index 0000000..9a6e74c Binary files /dev/null and b/lib_face/libs/armeabi-v7a/libarcsoft_face_engine.so differ diff --git a/lib_face/libs/armeabi-v7a/libarcsoft_image_util.so b/lib_face/libs/armeabi-v7a/libarcsoft_image_util.so new file mode 100644 index 0000000..b0ac1dd Binary files /dev/null and b/lib_face/libs/armeabi-v7a/libarcsoft_image_util.so differ diff --git a/lib_face/proguard-rules.pro b/lib_face/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/lib_face/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/lib_face/src/androidTest/java/com/sw/plate/ExampleInstrumentedTest.java b/lib_face/src/androidTest/java/com/sw/plate/ExampleInstrumentedTest.java new file mode 100644 index 0000000..99a82b5 --- /dev/null +++ b/lib_face/src/androidTest/java/com/sw/plate/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.sw.plate; + +import static org.junit.Assert.assertEquals; + +import android.content.Context; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.sw.plate.test", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/lib_face/src/main/AndroidManifest.xml b/lib_face/src/main/AndroidManifest.xml new file mode 100644 index 0000000..44008a4 --- /dev/null +++ b/lib_face/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/lib_face/src/main/java/com/sw/plate/App.java b/lib_face/src/main/java/com/sw/plate/App.java new file mode 100644 index 0000000..77caee7 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/App.java @@ -0,0 +1,19 @@ +package com.sw.plate; + +import android.app.Application; +import android.content.Context; + +public class App extends Application { + private static Context mContext; + + @Override + public void onCreate() { + super.onCreate(); + mContext = this; + } + + public static Context getContext() { + return mContext; + } + +} diff --git a/lib_face/src/main/java/com/sw/plate/AppConst.java b/lib_face/src/main/java/com/sw/plate/AppConst.java new file mode 100644 index 0000000..0e7be5a --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/AppConst.java @@ -0,0 +1,30 @@ +package com.sw.plate; + + +import android.os.Environment; + +public class AppConst { + + public static final String BASE_FILE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/sw"; + +// public static final String ARCSOFT_APP_ID = "J6jt8Lgou3cTW9Y1k9T8Zx4nP51ZgcHRv668znCcUu5g"; +// public static final String ARCSOFT_SDK_KEY = "8necG4J6MQeTnz4gvcZuaRUywJynZindJCt2geuBnYv9"; + + +// 85Q1-11DY-B13F-83WC +// APP_ID:H7kCBZ6zf8xMiqVXRmiXeaCaFhHGB5ubUiDkocQRydfQ +// SDK_KEY:7sLu3pXYUiBurhTJjWB5yWac8qYxjDTeR8iSqAG7dAnM + + public static final String ARCSOFT_APP_ID = "H7kCBZ6zf8xMiqVXRmiXeaCaFhHGB5ubUiDkocQRydfQ"; + public static final String ARCSOFT_SDK_KEY = "7sLu3pXYUiBurhTJjWB5yWac8qYxjDTeR8iSqAG7dAnM"; + public static final String ARCSOFT_ACTIVE_KEY = "85Q1-11DY-B13F-83WC"; + /** + * 方式二: 在激活界面读取本地配置文件进行激活 + *

+ * 配置文件名称,格式如下: + * APP_ID:XXXXXXXXXXXXX + * SDK_KEY:XXXXXXXXXXXXXXX + * ACTIVE_KEY:XXXX-XXXX-XXXX-XXXX + */ + public static final String ACTIVE_CONFIG_FILE_NAME = "activeConfig.txt"; +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/AppUtil.java b/lib_face/src/main/java/com/sw/plate/utils/AppUtil.java new file mode 100644 index 0000000..911374a --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/AppUtil.java @@ -0,0 +1,751 @@ +package com.sw.plate.utils; + +import static android.content.Context.TELEPHONY_SERVICE; + +import android.annotation.SuppressLint; +import android.bluetooth.BluetoothAdapter; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Build; +import android.provider.Settings; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; + +import androidx.core.content.FileProvider; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.LineNumberReader; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.net.NetworkInterface; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.UUID; + +public class AppUtil { + public static String getAppPackageName(Context context) { + String packageName = ""; + try { + PackageManager pm = context.getPackageManager(); + PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0); + packageName = pi.packageName; + if (AppUtil.isEmpty(packageName)) { + return ""; + } + } catch (Exception e) { + e.printStackTrace(); + } + return packageName; + } + + public static String getAppVersionName(Context context) { + String versionName = ""; + // int versioncode=1; + try { + PackageManager pm = context.getPackageManager(); + PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0); + versionName = pi.versionName; + // versioncode = pi.versionCode;表示更新了多少次 + if (versionName == null || versionName.length() <= 0) { + return ""; + } + } catch (Exception e) { + e.printStackTrace(); + } + return versionName; + } + + + public static int getAppVersionCode(Context context) { + int versioncode = 1; + try { + PackageManager pm = context.getPackageManager(); + PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0); + versioncode = pi.versionCode; + + } catch (Exception e) { + e.printStackTrace(); + } + return versioncode; + } + + //判断微信是否安装 + public static boolean isWeixinInstalled(Context context) { + final PackageManager packageManager = context.getPackageManager();// 获取packagemanager + List pinfo = packageManager.getInstalledPackages(0);// 获取所有已安装程序的包信息 + if (pinfo != null) { + for (int i = 0; i < pinfo.size(); i++) { + String pn = pinfo.get(i).packageName; + if (pn.equals("com.tencent.mm")) { + return true; + } + } + } + + return false; + } + + /** + * 打电话 + *

+ * Intent.ACTION_DIAL Intent.ACTION_CALL + * + * @param context + * @param mobile + */ + public static void callUp(Context context, String mobile) { + Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + + mobile)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + + /** + * 获取设备ID + * + * @param context + * @return + */ + public static String getDevId(Context context) { + TelephonyManager TelephonyMgr = (TelephonyManager) context + .getSystemService(Context.TELEPHONY_SERVICE); + return TelephonyMgr.getDeviceId(); + } + + /** + * 姓名脱敏 + * + * @param fullName + * @return + */ + public static String desensitizedName(String fullName) { + if (fullName == null || fullName.length() <= 1) { + return fullName; + } + char[] nameArr = fullName.toCharArray(); + if (nameArr.length > 2) { + for (int i = 1; i < nameArr.length - 1; i++) { + nameArr[i] = '*'; + } + } else { + nameArr[1] = '*'; + } + + return new String(nameArr); + } + + + public static String formatDateGetFull(String date) { + if (isEmpty(date)) { + return ""; + } + Date d = new Date(Long.parseLong(date)); + SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + return dateFormat1.format(d); + } + + public static String formatDateGetCurrentTime() { + Date d = new Date(System.currentTimeMillis()); + SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//.SSS + return dateFormat1.format(d); + } + + + public static String formatDateGetFull(long date) { + if (date == 0) { + return ""; + } + Date d = new Date(date); + SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + return dateFormat1.format(d); + } + + public static String formatDateGetDay(long date) { + if (date == 0) { + return ""; + } + Date d = new Date(date); + SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd"); + return dateFormat1.format(d); + } + + public static boolean isEmpty(String s) { + if (TextUtils.isEmpty(s) || s.trim().equals("null")) { + return true; + } else { + return false; + } + } + + + /** + * 格式化浮点型 + * + * @param data + * @return + */ + public static String formatDouble(double data) { + return new DecimalFormat("0.00").format(data); + } + + + /** + * 格式化分钟 + * + * @param minutes + * @return + */ + public static String formatMinutes(int minutes) { + int hour = minutes / 60; + int minute = minutes % 60; + if (hour > 0 && minute > 0) { + return hour + "小时" + minute + "分钟"; + } else if (hour > 0) { + return hour + "小时"; + } else { + return minute + "分钟"; + } + } + + + //com.fawan.news + public static void goToMarket(Context context, String packageName) { + Uri uri = Uri.parse("market://details?id=" + packageName); + Intent goToMarket = new Intent(Intent.ACTION_VIEW, uri); + try { + context.startActivity(goToMarket); + } catch (ActivityNotFoundException e) { + e.printStackTrace(); + } + } + + /** + * true为存在,false为不存在 + * + * @param context + * @param packageName + * @return + */ + public static boolean isInstallApp(Context context, String packageName) { + try { + context.getPackageManager().getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES); + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + /** + * 格式化float 保留两位小数 + * + * @param data + * @return + */ + public static float formatFloat2(float data) { +// DecimalFormat decimalFormat = new DecimalFormat("0.00");//构造方法的字符格式这里如果小数不足2位,会以0补足. +// return decimalFormat.format(data);//返回字符串 + + int scale = 1;//设置位数 + int roundingMode = 4;//表示四舍五入,可以选择其他舍值方式,例如去尾,等等. + BigDecimal bd = new BigDecimal((double) data); + bd = bd.setScale(scale, roundingMode); + data = bd.floatValue(); + return data; + } + + /** + * Android 6.0 之前(不包括6.0)获取mac地址 + * 必须的权限 + * + * @param context * @return + */ + public static String getMacDefault(Context context) { + String mac = ""; + if (context == null) { + return mac; + } + WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + WifiInfo info = null; + try { + info = wifi.getConnectionInfo(); + } catch (Exception e) { + e.printStackTrace(); + } + + if (info == null) { + return null; + } + mac = info.getMacAddress(); + if (!TextUtils.isEmpty(mac)) { + mac = mac.toUpperCase(Locale.ENGLISH); + } + return mac; + } + + /** + * Android 6.0-Android 7.0 获取mac地址 + */ + public static String getMacAddress() { + String macSerial = null; + String str = ""; + + try { + Process pp = Runtime.getRuntime().exec("cat/sys/class/net/wlan0/address"); + InputStreamReader ir = new InputStreamReader(pp.getInputStream()); + LineNumberReader input = new LineNumberReader(ir); + + while (null != str) { + str = input.readLine(); + if (str != null) { + macSerial = str.trim();//去空格 + break; + } + } + } catch (IOException ex) { + // 赋予默认值 + ex.printStackTrace(); + } + + return macSerial; + } + + /** + * Android 7.0之后获取Mac地址 + * 遍历循环所有的网络接口,找到接口是 wlan0 + * 必须的权限 + * + * @return + */ + public static String getMacFromHardware() { + try { + ArrayList all = Collections.list(NetworkInterface.getNetworkInterfaces()); + for (NetworkInterface nif : all) { + if (!nif.getName().equals("wlan0")) + continue; + byte[] macBytes = nif.getHardwareAddress(); + if (macBytes == null) return ""; + StringBuilder res1 = new StringBuilder(); + for (Byte b : macBytes) { + res1.append(String.format("%02X:", b)); + } + if (!TextUtils.isEmpty(res1)) { + res1.deleteCharAt(res1.length() - 1); + } + return res1.toString(); + } + } catch (Exception e) { + e.printStackTrace(); + } + + return ""; + } + + /** + * 获取mac地址(适配所有Android版本) + * + * @return + */ + public static String getMac(Context context) { + String mac = ""; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + mac = getMacDefault(context); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + mac = getMacAddress(); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mac = getMacFromHardware(); + } + return mac; + } + + //把String转化为float + public static double convertToFloat(String number, double defaultValue) { + if (TextUtils.isEmpty(number)) { + return defaultValue; + } + try { + return Double.parseDouble(number); + } catch (Exception e) { + return defaultValue; + } + + } + + /** + * 获取AndroidId + * + * @param context + * @return + */ + public static String getAndroidId(Context context) { + String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); + return androidId; + } + + /** + * 获取设备唯一 UDID + * + * @param context + * @return + */ + @SuppressLint("MissingPermission") + public static String getUDID(Context context) { +// String androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); +// L.e("androidID===" + androidID); +// return androidID; + String androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); + if (!androidID.equals("")) { + try { + if (!"9774d56d682e549c".equals(androidID)) { + androidID = UUID.nameUUIDFromBytes(androidID.getBytes("utf8")).toString(); + } else { + @SuppressLint("MissingPermission") final String deviceId = ((TelephonyManager) context.getSystemService(TELEPHONY_SERVICE)).getDeviceId(); + androidID = deviceId != null ? UUID.nameUUIDFromBytes(deviceId.getBytes("utf8")).toString() : UUID.randomUUID().toString(); + } + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + return androidID; + } + + //需要权限 android.permission.READ_PHONE_STATE + TelephonyManager TelephonyMgr = (TelephonyManager) context.getSystemService(TELEPHONY_SERVICE); + String szImei = TelephonyMgr.getDeviceId(); + if (!szImei.equals("")) { + return szImei; + } + + //需要权限 android.permission.ACCESS_WIFI_STATE + WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + String m_szWLANMAC = wm.getConnectionInfo().getMacAddress(); + if (!m_szWLANMAC.equals("")) { + return m_szWLANMAC; + } + + //需要权限 android.permission.BLUETOOTH + BluetoothAdapter m_BluetoothAdapter = null; + m_BluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + String m_szBTMAC = m_BluetoothAdapter.getAddress(); + if (!m_szBTMAC.equals("")) { + return m_szBTMAC; + } + return getUniquePsuedoID(); + } + + //获得 Psuedo ID + public static String getUniquePsuedoID() { + String serial = null; + String m_szDevIDShort = "35" + + Build.BOARD.length() % 10 + Build.BRAND.length() % 10 + + Build.CPU_ABI.length() % 10 + Build.DEVICE.length() % 10 + + Build.DISPLAY.length() % 10 + Build.HOST.length() % 10 + + Build.ID.length() % 10 + Build.MANUFACTURER.length() % 10 + + Build.MODEL.length() % 10 + Build.PRODUCT.length() % 10 + + Build.TAGS.length() % 10 + Build.TYPE.length() % 10 + + Build.USER.length() % 10; //13 位 + try { + serial = Build.class.getField("SERIAL").get(null).toString(); + //API>=9 使用serial号 + return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString(); + } catch (Exception exception) { + //serial需要一个初始化,随意值 + serial = "serial"; + } + + //使用硬件信息拼凑出来的15位号码 + return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString(); + } + + public static String getCPUSerial() { + String line = ""; + String TAG = "aaa"; + Log.e(TAG, " get_quck_Sn() "); + Class c = null; + try { + c = Class.forName("android.os.SystemProperties"); + + Method get = c.getMethod("get", String.class); + line = (String) get.invoke(c, "ro.serialno"); + } catch (ClassNotFoundException | NoSuchMethodException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + + Log.e(TAG, " get_quck_Sn() " + line); + System.out.println("设备串号" + line); + return line; + } + + /** + * 判断网络连接状态 + * + * @param context + * @return + */ + public static boolean isNetworkConnected(Context context) { + if (context != null) { + ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo(); + if (mNetworkInfo != null) { + return mNetworkInfo.isAvailable(); + } + } + return false; + } + + /** + * 判断WiFi连接状态 + * + * @param context + * @return + */ + public static boolean isWifiConnected(Context context) { + if (context != null) { + ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo mWiFiNetworkInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + if (mWiFiNetworkInfo != null) { + return mWiFiNetworkInfo.isAvailable(); + } + } + return false; + } + + /** + * 判断移动网络状态 + * + * @param context + * @return + */ + public static boolean isMobileConnected(Context context) { + if (context != null) { + ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo mMobileNetworkInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE); + if (mMobileNetworkInfo != null) { + return mMobileNetworkInfo.isAvailable(); + } + } + return false; + } + + /** + * 获取网络连接类型 + * + * @param context + * @return + */ + public static int getConnectedType(Context context) { + if (context != null) { + ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo(); + if (mNetworkInfo != null && mNetworkInfo.isAvailable()) { + return mNetworkInfo.getType(); + } + } + return -1; + } + + /** + * 根据字符的起始和结束索引提取子串 + * + * @param input 原始字符串 + * @param startIndex 起始索引(包含,从0开始) + * @param endIndex 结束索引(不包含) + * @return 子串,若输入无效或索引越界则返回空字符串 + */ + public static String getSubstringByIndices(String input, int startIndex, int endIndex) { + if (input == null) { + return ""; + } + // 处理索引越界问题 + int safeStart = Math.max(startIndex, 0); + int safeEnd = Math.min(endIndex, input.length()); + if (safeStart > safeEnd) { + return ""; + } + return input.substring(safeStart, safeEnd); + } + + public static String getSubstringByIndex(String input, int startIndex, int length) { + if (input == null) { + return ""; + } + // 处理索引越界问题 + int safeStart = Math.max(startIndex, 0); + int safeEnd = Math.min(startIndex + length, input.length()); + if (safeStart > safeEnd) { + return ""; + } + return input.substring(safeStart, safeEnd); + } + + /** + * 十进制转十六进制 + * + * @param decimal + * @return + */ + public static String decimalToHexWithPadding(int decimal, int padding) { + // 将十进制转换为十六进制,并转换为字符串 + String hex = Integer.toHexString(decimal); + + // 确保字符串长度为至少4位,不足部分前面补0 + while (hex.length() < padding) { + hex = "0" + hex; + } + + return hex.toUpperCase(); // 返回大写形式的十六进制字符串 + } + + /** + * 十进制转二进制,且返回的二进制为至少7位数 + * + * @param decimal + * @return + */ + public static String decimalToBinary(int decimal) { + // 如果输入为0,直接返回"0" + if (decimal == 0) { + return "0"; + } + + StringBuilder binary = new StringBuilder(); + + // 除2取余法,将余数加入二进制字符串 + while (decimal > 0) { + int remainder = decimal % 2; + binary.insert(0, remainder); + decimal = decimal / 2; + } + int length = binary.length(); + if (length < 7) { + int padding = 7 - length; + for (int i = 0; i < padding; i++) { + binary.insert(0, '0'); + } + } + return binary.toString(); + } + + /** + * 将二进制字符串转换为十六进制字符串,每8位转换为两位十六进制,不足两位前面补零 + * + * @param binaryStr 输入的二进制字符串(仅包含0和1) + * @return 转换后的十六进制字符串 + * @throws IllegalArgumentException 如果输入不是有效的二进制字符串 + */ + public static String binaryToHex(String binaryStr) { + // 校验输入合法性 + if (binaryStr == null || !binaryStr.matches("[01]+")) { + throw new IllegalArgumentException("Invalid binary string"); + } + + // 补前导零使长度成为8的倍数 + int length = binaryStr.length(); + int padding = (8 - (length % 8)) % 8; // 计算需要补零的数量 + StringBuilder paddedBinary = new StringBuilder(); + for (int i = 0; i < padding; i++) { + paddedBinary.append('0'); + } + paddedBinary.append(binaryStr); + + // 每8位转换为两位十六进制 + StringBuilder hexStr = new StringBuilder(); + for (int i = 0; i < paddedBinary.length(); i += 8) { + String byteStr = paddedBinary.substring(i, i + 8); + int decimalValue = Integer.parseInt(byteStr, 2); + hexStr.append(String.format("%02X", decimalValue & 0xFF)); + } + + return hexStr.toString(); + } + + /** + * 数据校验 异或处理 + */ + public static String getXor(String content) { + int a = 0; + for (int i = 0; i < content.length() / 2; i++) { + a = a ^ Integer.parseInt(content.substring(i * 2, (i * 2) + 2), 16); + } + String result = Integer.toHexString(a).toUpperCase(); + if (result.length() == 1) { + return "0" + result; + } else { + return result; + } + } + + public static double formatPersonInfo(String input, int startIndex, int length) { + if (input == null) { + return 0; + } + // 处理索引越界问题 + int safeStart = Math.max(startIndex, 0); + int safeEnd = Math.min(startIndex + length, input.length()); + if (safeStart > safeEnd) { + return 0; + } + String result = input.substring(safeStart, safeEnd); + double num = Integer.parseInt(result, 16); + return num; + } + + /** + * 安装apk + * + * @param activity + * @param apkFile + */ + public static void installApk(Context activity, File apkFile) { + //文件有所有者概念,现在是属于当前进程的,需要把这个文件暴露给系统安装程序(其他进程)去安装 + //因此,可能会存在权限问题,需要做下面的设置 + //如果文件是sdcard上的,就不需要这个操作了 + try { + apkFile.setExecutable(true, false); + apkFile.setReadable(true, false); + apkFile.setWritable(true, false); + } catch (Exception e) { + e.printStackTrace(); + } + + Intent intent = new Intent(); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setAction(Intent.ACTION_VIEW); + Uri uri; + + //TODO N FileProvider + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + uri = FileProvider.getUriForFile(activity, activity.getPackageName() + ".fileProvider", apkFile); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); +// intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } else { + uri = Uri.fromFile(apkFile); + } + + intent.setDataAndType(uri, "application/vnd.android.package-archive"); + activity.startActivity(intent); + + //TODO 0 INSTALL PERMISSION + //在AndroidManifest中加入权限即可 + } + +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/Base64.java b/lib_face/src/main/java/com/sw/plate/utils/Base64.java new file mode 100644 index 0000000..eaf28da --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/Base64.java @@ -0,0 +1,265 @@ +package com.sw.plate.utils; + +public final class Base64 { + + private static final int BASELENGTH = 128; + private static final int LOOKUPLENGTH = 64; + private static final int TWENTYFOURBITGROUP = 24; + private static final int EIGHTBIT = 8; + private static final int SIXTEENBIT = 16; + private static final int FOURBYTE = 4; + private static final int SIGN = -128; + private static char PAD = '='; + private static byte[] base64Alphabet = new byte[BASELENGTH]; + private static char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH]; + + static { + for (int i = 0; i < BASELENGTH; ++i) { + base64Alphabet[i] = -1; + } + for (int i = 'Z'; i >= 'A'; i--) { + base64Alphabet[i] = (byte) (i - 'A'); + } + for (int i = 'z'; i >= 'a'; i--) { + base64Alphabet[i] = (byte) (i - 'a' + 26); + } + + for (int i = '9'; i >= '0'; i--) { + base64Alphabet[i] = (byte) (i - '0' + 52); + } + + base64Alphabet['+'] = 62; + base64Alphabet['/'] = 63; + + for (int i = 0; i <= 25; i++) { + lookUpBase64Alphabet[i] = (char) ('A' + i); + } + + for (int i = 26, j = 0; i <= 51; i++, j++) { + lookUpBase64Alphabet[i] = (char) ('a' + j); + } + + for (int i = 52, j = 0; i <= 61; i++, j++) { + lookUpBase64Alphabet[i] = (char) ('0' + j); + } + lookUpBase64Alphabet[62] = (char) '+'; + lookUpBase64Alphabet[63] = (char) '/'; + + } + + private static boolean isWhiteSpace(char octect) { + return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9); + } + + private static boolean isPad(char octect) { + return (octect == PAD); + } + + private static boolean isData(char octect) { + return (octect < BASELENGTH && base64Alphabet[octect] != -1); + } + + /** + * Encodes hex octects into Base64 + * + * @param binaryData Array containing binaryData + * @return Encoded Base64 array + */ + public static String encode(byte[] binaryData) { + + if (binaryData == null) { + return null; + } + + int lengthDataBits = binaryData.length * EIGHTBIT; + if (lengthDataBits == 0) { + return ""; + } + + int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP; + int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP; + int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 + : numberTriplets; + char encodedData[] = null; + + encodedData = new char[numberQuartet * 4]; + + byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0; + + int encodedIndex = 0; + int dataIndex = 0; + + for (int i = 0; i < numberTriplets; i++) { + b1 = binaryData[dataIndex++]; + b2 = binaryData[dataIndex++]; + b3 = binaryData[dataIndex++]; + + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) + : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) + : (byte) ((b2) >> 4 ^ 0xf0); + byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) + : (byte) ((b3) >> 6 ^ 0xfc); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f]; + } + + // form integral number of 6-bit groups + if (fewerThan24bits == EIGHTBIT) { + b1 = binaryData[dataIndex]; + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) + : (byte) ((b1) >> 2 ^ 0xc0); + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4]; + encodedData[encodedIndex++] = PAD; + encodedData[encodedIndex++] = PAD; + } else if (fewerThan24bits == SIXTEENBIT) { + b1 = binaryData[dataIndex]; + b2 = binaryData[dataIndex + 1]; + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) + : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) + : (byte) ((b2) >> 4 ^ 0xf0); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2]; + encodedData[encodedIndex++] = PAD; + } + + return new String(encodedData); + } + + /** + * Decodes Base64 data into octects + * + * @param encoded string containing Base64 data + * @return Array containind decoded data. + */ + public static byte[] decode(String encoded) { + + if (encoded == null) { + return null; + } + + char[] base64Data = encoded.toCharArray(); + // remove white spaces + int len = removeWhiteSpace(base64Data); + + if (len % FOURBYTE != 0) { + return null;// should be divisible by four + } + + int numberQuadruple = (len / FOURBYTE); + + if (numberQuadruple == 0) { + return new byte[0]; + } + + byte decodedData[] = null; + byte b1 = 0, b2 = 0, b3 = 0, b4 = 0; + char d1 = 0, d2 = 0, d3 = 0, d4 = 0; + + int i = 0; + int encodedIndex = 0; + int dataIndex = 0; + decodedData = new byte[(numberQuadruple) * 3]; + + for (; i < numberQuadruple - 1; i++) { + + if (!isData((d1 = base64Data[dataIndex++])) + || !isData((d2 = base64Data[dataIndex++])) + || !isData((d3 = base64Data[dataIndex++])) + || !isData((d4 = base64Data[dataIndex++]))) { + return null; + }// if found "no data" just return null + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + } + + if (!isData((d1 = base64Data[dataIndex++])) + || !isData((d2 = base64Data[dataIndex++]))) { + return null;// if found "no data" just return null + } + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + + d3 = base64Data[dataIndex++]; + d4 = base64Data[dataIndex++]; + if (!isData((d3)) || !isData((d4))) {// Check if they are PAD characters + if (isPad(d3) && isPad(d4)) { + if ((b2 & 0xf) != 0)// last 4 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 1]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + return tmp; + } else if (!isPad(d3) && isPad(d4)) { + b3 = base64Alphabet[d3]; + if ((b3 & 0x3) != 0)// last 2 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 2]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + return tmp; + } else { + return null; + } + } else { // No PAD e.g 3cQl + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + + } + + return decodedData; + } + + /** + * remove WhiteSpace from MIME containing encoded Base64 data. + * + * @param data the byte array of base64 data (with WS) + * @return the new length + */ + private static int removeWhiteSpace(char[] data) { + if (data == null) { + return 0; + } + + // count characters that's not whitespace + int newSize = 0; + int len = data.length; + for (int i = 0; i < len; i++) { + if (!isWhiteSpace(data[i])) { + data[newSize++] = data[i]; + } + } + return newSize; + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/ByteUtil.java b/lib_face/src/main/java/com/sw/plate/utils/ByteUtil.java new file mode 100644 index 0000000..d28529c --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/ByteUtil.java @@ -0,0 +1,179 @@ +package com.sw.plate.utils; + +public class ByteUtil { + /** + * 字节数组转换成对应的16进制表示的字符串 + * + * @param src + * @return + */ + public static String bytes2HexStr(byte[] src) { + StringBuilder builder = new StringBuilder(); + if (src == null || src.length <= 0) { + return ""; + } + char[] buffer = new char[2]; + for (int i = 0; i < src.length; i++) { + buffer[0] = Character.forDigit((src[i] >>> 4) & 0x0F, 16); + buffer[1] = Character.forDigit(src[i] & 0x0F, 16); + builder.append(buffer); + } + return builder.toString().toUpperCase(); + } + + /** + * 十六进制字节数组转字符串 + * + * @param src 目标数组 + * @param dec 起始位置 + * @param length 长度 + * @return + */ + public static String bytes2HexStr(byte[] src, int dec, int length) { + byte[] temp = new byte[length]; + System.arraycopy(src, dec, temp, 0, length); + return bytes2HexStr(temp); + } + + /** + * 16进制字符串转10进制数字 + * + * @param hex + * @return + */ + public static long hexStr2decimal(String hex) { + return Long.parseLong(hex, 16); + } + + /** + * 把十进制数字转换成足位的十六进制字符串,并补全空位 + * + * @param num + * @return + */ + public static String decimal2fitHex(long num) { + String hex = Long.toHexString(num).toUpperCase(); + if (hex.length() % 2 != 0) { + return "0" + hex; + } + return hex.toUpperCase(); + } + + /** + * 把十进制数字转换成足位的十六进制字符串,并补全空位 + * + * @param num + * @param strLength 字符串的长度 + * @return + */ + public static String decimal2fitHex(long num, int strLength) { + String hexStr = decimal2fitHex(num); + StringBuilder stringBuilder = new StringBuilder(hexStr); + while (stringBuilder.length() < strLength) { + stringBuilder.insert(0, '0'); + } + return stringBuilder.toString(); + } + + public static String fitDecimalStr(int dicimal, int strLength) { + StringBuilder builder = new StringBuilder(String.valueOf(dicimal)); + while (builder.length() < strLength) { + builder.insert(0, "0"); + } + return builder.toString(); + } + + /** + * 字符串转十六进制字符串 + * + * @param str + * @return + */ + public static String str2HexString(String str) { + char[] chars = "0123456789ABCDEF".toCharArray(); + StringBuilder sb = new StringBuilder(); + byte[] bs = null; + try { + + bs = str.getBytes("utf8"); + } catch (Exception e) { + e.printStackTrace(); + } + int bit; + for (int i = 0; i < bs.length; i++) { + bit = (bs[i] & 0x0f0) >> 4; + sb.append(chars[bit]); + bit = bs[i] & 0x0f; + sb.append(chars[bit]); + } + return sb.toString(); + } + + /** + * 把十六进制表示的字节数组字符串,转换成十六进制字节数组 + * + * @param + * @return byte[] + */ + public static byte[] hexStr2bytes(String hex) { + int len = (hex.length() / 2); + byte[] result = new byte[len]; + char[] achar = hex.toUpperCase().toCharArray(); + for (int i = 0; i < len; i++) { + int pos = i * 2; + result[i] = (byte) (hexChar2byte(achar[pos]) << 4 | hexChar2byte(achar[pos + 1])); + } + return result; + } + + /** + * 把16进制字符[0123456789abcde](含大小写)转成字节 + * + * @param c + * @return + */ + private static int hexChar2byte(char c) { + switch (c) { + case '0': + return 0; + case '1': + return 1; + case '2': + return 2; + case '3': + return 3; + case '4': + return 4; + case '5': + return 5; + case '6': + return 6; + case '7': + return 7; + case '8': + return 8; + case '9': + return 9; + case 'a': + case 'A': + return 10; + case 'b': + case 'B': + return 11; + case 'c': + case 'C': + return 12; + case 'd': + case 'D': + return 13; + case 'e': + case 'E': + return 14; + case 'f': + case 'F': + return 15; + default: + return -1; + } + } +} \ No newline at end of file diff --git a/lib_face/src/main/java/com/sw/plate/utils/CabinetLockCommand.java b/lib_face/src/main/java/com/sw/plate/utils/CabinetLockCommand.java new file mode 100644 index 0000000..a7401fd --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/CabinetLockCommand.java @@ -0,0 +1,230 @@ +package com.sw.plate.utils; + +import java.util.HashMap; +import java.util.Map; + +public class CabinetLockCommand { + /** + * 生成开柜指令(含校验位) + * + * @param boxNumber 柜门号(1-65535) + * @return 十六进制格式指令字符串,如 "5A2100017A" + */ + public static String generateOpenCommand(int boxNumber) { + if (boxNumber < 1 || boxNumber > 0xFFFF) { + throw new IllegalArgumentException("柜门号范围应为1-65535"); + } + + // 固定头+功能码 + byte head = 0x5A; + byte functionCode = 0x21; + + // 大端序箱门号(2字节) + byte[] boxCh = { + (byte) ((boxNumber >> 8) & 0xFF), + (byte) (boxNumber & 0xFF) + }; + + // 计算异或校验(head + functionCode + boxCh) + byte xorCheck = head; + xorCheck ^= functionCode; + xorCheck ^= boxCh[0]; + xorCheck ^= boxCh[1]; + + // 拼接完整指令 + return String.format("%02X%02X%02X%02X%02X", + head, functionCode, boxCh[0], boxCh[1], xorCheck); + } + + + /** + * 生成查询开关门指令(含校验位) + * + * @param boxNumber 柜门号(1-65535) + * @return 十六进制格式指令字符串,如 "5A2100017A" + */ + public static String generateBoxStatusCommand(int boxNumber) { + if (boxNumber < 1 || boxNumber > 0xFFFF) { + throw new IllegalArgumentException("柜门号范围应为1-65535"); + } + + // 固定头+功能码 + byte head = 0x5A; + byte functionCode = 0x22; + + // 大端序箱门号(2字节) + byte[] boxCh = { + (byte) ((boxNumber >> 8) & 0xFF), + (byte) (boxNumber & 0xFF) + }; + + // 计算异或校验(head + functionCode + boxCh) + byte xorCheck = head; + xorCheck ^= functionCode; + xorCheck ^= boxCh[0]; + xorCheck ^= boxCh[1]; + + // 拼接完整指令 + return String.format("%02X%02X%02X%02X%02X", + head, functionCode, boxCh[0], boxCh[1], xorCheck); + } + + /** + * 生成查询是否存放指令(含校验位) + * + * @param boxNumber 柜门号(1-65535) + * @return 十六进制格式指令字符串,如 "5A2100017A" + */ + public static String generateBoxHasCommand(int boxNumber) { + if (boxNumber < 1 || boxNumber > 0xFFFF) { + throw new IllegalArgumentException("柜门号范围应为1-65535"); + } + + // 固定头+功能码 + byte head = 0x5A; + byte functionCode = 0x25; + + // 大端序箱门号(2字节) + byte[] boxCh = { + (byte) ((boxNumber >> 8) & 0xFF), + (byte) (boxNumber & 0xFF) + }; + + // 计算异或校验(head + functionCode + boxCh) + byte xorCheck = head; + xorCheck ^= functionCode; + xorCheck ^= boxCh[0]; + xorCheck ^= boxCh[1]; + + // 拼接完整指令 + return String.format("%02X%02X%02X%02X%02X", + head, functionCode, boxCh[0], boxCh[1], xorCheck); + } + + private static final byte TURN_ON = (byte) 0xB1; + private static final byte TURN_OFF = (byte) 0xB2; + private static final byte TURN_UVC_ON = (byte) 0xB3; + private static final byte TURN_UVC_OFF = (byte) 0xB4; + + /** + * 生成灯光控制指令 + * + * @param deviceNumber 设备号(1-255) + * @param isTurnOn true=开灯, false=关灯 + * @return 十六进制指令字符串 + */ + public static String generateLightCommand(int deviceNumber, boolean isTurnOn) { + if (deviceNumber < 1 || deviceNumber > 255) { + throw new IllegalArgumentException("设备号范围应为1-255"); + } + + byte[] command = new byte[5]; + command[0] = 0x55; + command[1] = (byte) deviceNumber; + command[2] = isTurnOn ? TURN_ON : TURN_OFF; + command[3] = 0x5F; + command[4] = 0x00; + + // 计算校验位 + byte checksum = command[0]; + for (int i = 1; i < command.length - 1; i++) { + checksum ^= command[i]; + } + command[command.length - 1] = checksum; + + // 转换为十六进制字符串 + StringBuilder sb = new StringBuilder(); + for (byte b : command) { + sb.append(String.format("%02X", b)); + } + return sb.toString().trim(); + } + + /** + * 生成紫外线灯光控制指令 + * + * @param deviceNumber 设备号(1-255) + * @param isTurnOn true=开灯, false=关灯 + * @return 十六进制指令字符串 + */ + public static String generateUVCLightCommand(int deviceNumber, boolean isTurnOn) { + if (deviceNumber < 1 || deviceNumber > 255) { + throw new IllegalArgumentException("设备号范围应为1-255"); + } + + byte[] command = new byte[5]; + command[0] = 0x55; + command[1] = (byte) deviceNumber; + command[2] = isTurnOn ? TURN_UVC_ON : TURN_UVC_OFF; + command[3] = 0x5F; + command[4] = 0x00; + + // 计算校验位 + byte checksum = command[0]; + for (int i = 1; i < command.length - 1; i++) { + checksum ^= command[i]; + } + command[command.length - 1] = checksum; + + // 转换为十六进制字符串 + StringBuilder sb = new StringBuilder(); + for (byte b : command) { + sb.append(String.format("%02X", b)); + } + return sb.toString().trim(); + } + + /** + * 解析箱门状态数据 + * + * @param data 原始数据字符串,如"5AA2000100161008E017" + * @return 包含所有箱门状态的Map,key为箱门号,value为开关状态(true=开) + */ + public static Map parseBoxStatus(String data) { + Map statusMap = new HashMap<>(); + + // 验证数据长度至少要有10个字符(5字节) + if (data == null || data.length() < 10) { + return statusMap; + } + + try { + // 解析起始箱号和结束箱号 + int startBox = Integer.parseInt(data.substring(4, 8), 16); + int endBox = Integer.parseInt(data.substring(8, 12), 16); + + // 计算箱门总数和需要的字节数 + int boxCount = endBox - startBox + 1; + int byteCount = (boxCount + 7) / 8; + + // 验证数据长度是否足够 + if (data.length() < 12 + byteCount * 2) { + return statusMap; + } + + // 解析状态字节 + String stateStr = data.substring(12, 12 + byteCount * 2); + + // 处理每个字节 + for (int i = 0; i < byteCount; i++) { + // 获取当前字节(低字节在前) + String byteStr = stateStr.substring(i * 2, i * 2 + 2); + int byteValue = Integer.parseInt(byteStr, 16); + + // 处理字节中的每一位 + for (int bit = 0; bit < 8; bit++) { + int boxNum = startBox + i * 8 + bit; + if (boxNum > endBox) break; + + boolean isOpen = ((byteValue >> bit) & 0x01) == 0x01; + statusMap.put(boxNum, isOpen); + } + } + + } catch (NumberFormatException e) { + e.printStackTrace(); + } + + return statusMap; + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/GpioUtils.java b/lib_face/src/main/java/com/sw/plate/utils/GpioUtils.java new file mode 100644 index 0000000..ed6e82f --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/GpioUtils.java @@ -0,0 +1,175 @@ +package com.sw.plate.utils; + +import android.os.SystemClock; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.DataOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; + +/** + * Created by Administrator on 2018/11/9. + */ +public class GpioUtils { + + private static final String TAG = "GpioUtils"; + + /* + 给export申请权限 + */ + public static void upgradeRootPermissionForExport() { + upgradeRootPermission("/sys/class/gpio/export"); + } + + /* + 配置一个gpio路径 + */ + public static boolean exportGpio(int gpio) { + return writeNode("/sys/class/gpio/export", "" + gpio); + } + + /* + 给获取io口的状态的路径申请权限,该方法需要在exportGpio后调用 + */ + public static void upgradeRootPermissionForGpio(int gpio) { + upgradeRootPermission("/sys/class/gpio/gpio" + gpio + "/direction"); + upgradeRootPermission("/sys/class/gpio/gpio" + gpio + "/value"); + } + + + /* + 设置io口为输入或输出 + */ + public static boolean setGpioDirection(int gpio, int arg) { + String gpioDirection = ""; + if (arg == 0) gpioDirection = "out"; + else if (arg == 1) gpioDirection = "in"; + else return false; + return writeNode("/sys/class/gpio/gpio" + gpio + "/direction", gpioDirection); + } + + /* + 获取io口的状态为输出还是输入 + */ + public static String getGpioDirection(int gpio) { + String gpioDirection = ""; + gpioDirection = readNode("/sys/class/gpio/gpio" + gpio + "/direction"); + return gpioDirection; + } + + /* + 给输出io口写值,高电平或低电平 + */ + public static boolean writeGpioValue(int gpio, String arg) { + return writeNode("/sys/class/gpio/gpio" + gpio + "/value", arg); + } + + //获取当前gpio是高还是低 + public static String getGpioValue(int gpio) { + return readNode("/sys/class/gpio/gpio" + gpio + "/value"); + } + + + private static boolean upgradeRootPermission(String path) { + Process process = null; + DataOutputStream os = null; + try { + String cmd = "chmod 777 " + path; + process = Runtime.getRuntime().exec("su"); //切换到root帐号 + os = new DataOutputStream(process.getOutputStream()); + os.writeBytes(cmd + "\n"); + os.writeBytes("exit\n"); + os.flush(); + process.waitFor(); + } catch (Exception e) { + } finally { + try { + if (os != null) { + os.close(); + } + process.destroy(); + } catch (Exception e) { + } + } + try { + return process.waitFor() == 0; + } catch (InterruptedException e) { + e.printStackTrace(); + } + return false; + } + + private static boolean writeNode(String path, String arg) { + Log.d(TAG, "Gpio_test set node path: " + path + " arg: " + arg); + if (path == null || arg == null) { + Log.e(TAG, "set node error"); + return false; + } + FileWriter fileWriter = null; + BufferedWriter bufferedWriter = null; + try { + fileWriter = new FileWriter(path); + fileWriter.write(arg); + } catch (Exception e) { + Log.e(TAG, "Gpio_test write node error!! path" + path + " arg: " + arg); + e.printStackTrace(); + return false; + } finally { + try { + if (fileWriter != null) { + fileWriter.close(); + } + if (bufferedWriter != null) { + bufferedWriter.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + return true; + } + + private static long mTime = 0; + private static int mFailTimes = 0; + + private static String readNode(String path) { + String result = ""; + + FileReader fread = null; + BufferedReader buffer = null; + try { + fread = new FileReader(path); + buffer = new BufferedReader(fread); + String str = null; + while ((str = buffer.readLine()) != null) { + result = str; + break; + } + mFailTimes = 0; + } catch (IOException e) { + Log.e(TAG, "IO Exception"); + e.printStackTrace(); + if (mTime == 0 || SystemClock.uptimeMillis() - mTime < 1000) { + mFailTimes++; + } + if (mFailTimes >= 3) { + Log.d(TAG, "read format node continuous failed three times, exist thread"); + } + } finally { + try { + if (buffer != null) { + buffer.close(); + } + if (fread != null) { + fread.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + return result; + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/L.java b/lib_face/src/main/java/com/sw/plate/utils/L.java new file mode 100644 index 0000000..4ce67fe --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/L.java @@ -0,0 +1,57 @@ +package com.sw.plate.utils; + +/** + * Log统一管理类 + */ +public class L { + + private L() { + /* cannot be instantiated */ + throw new UnsupportedOperationException("cannot be instantiated"); + } + + public static boolean isDebug = true;// 是否需要打印bug,可以在application的onCreate函数里面初始化 + private static final String TAG = "mzf"; + + // 下面四个是默认tag的函数 + public static void i(String msg) { + if (isDebug) + android.util.Log.i(TAG, msg); + } + + public static void d(String msg) { + if (isDebug) + android.util.Log.d(TAG, msg); + } + + public static void e(String msg) { + if (isDebug) + android.util.Log.e(TAG, msg); + } + + public static void v(String msg) { + if (isDebug) + android.util.Log.v(TAG, msg); + } + + // 下面是传入自定义tag的函数 + public static void i(String tag, String msg) { + if (isDebug) + android.util.Log.i(tag, msg); + } + + public static void d(String tag, String msg) { + if (isDebug) + android.util.Log.d(tag, msg); + } + + public static void e(String tag, String msg) { + if (isDebug) + android.util.Log.e(tag, msg); + } + + public static void v(String tag, String msg) { + if (isDebug) + android.util.Log.v(tag, msg); + } +} \ No newline at end of file diff --git a/lib_face/src/main/java/com/sw/plate/utils/LightManager.java b/lib_face/src/main/java/com/sw/plate/utils/LightManager.java new file mode 100644 index 0000000..be8c13e --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/LightManager.java @@ -0,0 +1,25 @@ +package com.sw.plate.utils; + +public class LightManager { + private static final String TAG = "LightManager"; + + public static void openGreenLight() { + L.e(TAG, "openGreenLight"); + GpioUtils.writeGpioValue(41, "1"); + } + + public static void closeGreenLight() { + L.e(TAG, "closeGreenLight"); + GpioUtils.writeGpioValue(41, "0"); + } + + public static void openRedLight() { + L.e(TAG, "openRedLight"); + GpioUtils.writeGpioValue(40, "1"); + } + + public static void closeRedLight() { + L.e(TAG, "closeRedLight"); + GpioUtils.writeGpioValue(40, "0"); + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/NV21ToBitmap.java b/lib_face/src/main/java/com/sw/plate/utils/NV21ToBitmap.java new file mode 100644 index 0000000..871c1bc --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/NV21ToBitmap.java @@ -0,0 +1,64 @@ +package com.sw.plate.utils; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.renderscript.Allocation; +import android.renderscript.Element; +import android.renderscript.RenderScript; +import android.renderscript.ScriptIntrinsicYuvToRGB; +import android.renderscript.Type; + +public class NV21ToBitmap { + private RenderScript rs; + private ScriptIntrinsicYuvToRGB yuvToRgbIntrinsic; + private Type.Builder yuvType, rgbaType; + private Allocation in, out; + + public NV21ToBitmap(Context context) { + rs = RenderScript.create(context); + yuvToRgbIntrinsic = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs)); + } + + public Bitmap nv21ToBitmap(byte[] nv21, int width, int height, int orientationDegree) { + if (yuvType == null) { + yuvType = new Type.Builder(rs, Element.U8(rs)).setX(nv21.length); + in = Allocation.createTyped(rs, yuvType.create(), Allocation.USAGE_SCRIPT); + + rgbaType = new Type.Builder(rs, Element.RGBA_8888(rs)).setX(width).setY(height); + out = Allocation.createTyped(rs, rgbaType.create(), Allocation.USAGE_SCRIPT); + } + + in.copyFrom(nv21); + + yuvToRgbIntrinsic.setInput(in); + yuvToRgbIntrinsic.forEach(out); + +// Matrix m = new Matrix(); +// m.postScale(1,-1); +// m.postRotate(90, (float) width / 2, (float) height / 2); +// m.setRotate(90, (float) width / 2, (float) height / 2); + + Bitmap bmpout = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + out.copyTo(bmpout); + if (orientationDegree == 0) { + return bmpout; + } else { + return adjustPhotoRotation(bmpout, orientationDegree); + } + + } + + Bitmap adjustPhotoRotation(Bitmap bm, final int orientationDegree) { + Matrix m = new Matrix(); + m.setRotate(orientationDegree, (float) bm.getWidth() / 2, (float) bm.getHeight() / 2); + try { + Bitmap bm1 = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), m, true); + return bm1; + } catch (OutOfMemoryError ex) { + } + return null; + } + + +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/PrefUtils.java b/lib_face/src/main/java/com/sw/plate/utils/PrefUtils.java new file mode 100644 index 0000000..f40ca2b --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/PrefUtils.java @@ -0,0 +1,68 @@ +package com.sw.plate.utils; + +import android.content.Context; +import android.content.SharedPreferences; + +public class PrefUtils { + + public static final String PREF_NAME = "sw_selforder"; + + public static boolean getBoolean(Context ctx, String key, + boolean defaultValue) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, + Context.MODE_PRIVATE); + return sp.getBoolean(key, defaultValue); + } + + public static void setBoolean(Context ctx, String key, boolean value) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, + Context.MODE_PRIVATE); + sp.edit().putBoolean(key, value).commit(); + } + + public static String getString(Context ctx, String key, String defaultValue) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, + Context.MODE_PRIVATE); + return sp.getString(key, defaultValue); + } + + public static void setString(Context ctx, String key, String value) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, + Context.MODE_PRIVATE); + sp.edit().putString(key, value).commit(); + } + + public static int getInt(Context ctx, String key, int defaultValue) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, + Context.MODE_PRIVATE); + return sp.getInt(key, defaultValue); + } + + public static void setInt(Context ctx, String key, int value) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, + Context.MODE_PRIVATE); + sp.edit().putInt(key, value).commit(); + } + + public static float getFloat(Context ctx, String key, float defaultValue) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, + Context.MODE_PRIVATE); + return sp.getFloat(key, defaultValue); + } + + public static void setFloat(Context ctx, String key, float value) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, + Context.MODE_PRIVATE); + sp.edit().putFloat(key, value).commit(); + } + + public static void clearData(Context ctx, String key) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + sp.edit().remove(key).clear().commit(); + } + + public static void clearAllData(Context ctx) { + SharedPreferences sp = ctx.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + sp.edit().clear().commit(); + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/ScanGunKeyEventHelper.java b/lib_face/src/main/java/com/sw/plate/utils/ScanGunKeyEventHelper.java new file mode 100644 index 0000000..2b64fd0 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/ScanGunKeyEventHelper.java @@ -0,0 +1,166 @@ +package com.sw.plate.utils; + +import static android.content.Context.INPUT_SERVICE; + +import android.content.Context; +import android.hardware.input.InputManager; +import android.os.Handler; +import android.text.TextUtils; +import android.view.KeyEvent; + +/** + * 扫描枪事件处理 + */ +public class ScanGunKeyEventHelper { + private final static long MESSAGE_DELAY = 500; //延迟500ms,判断扫码是否完成。 + private final StringBuffer mStringBufferResult; //扫码内容 + private boolean mCaps; //大小写区分 + private final Handler mHandler; + private final Runnable mScanningFishedRunnable; + private OnScanSuccessListener mOnScanSuccessListener; + private final Context mContext; + + // private String mDeviceName = "TMC HIDKeyBoard"; + private String mDeviceName = "Linux 3.4.35 with ak-hsudc Composite Gadget (ACM + HID)"; + private String mDeviceName1 = "USBKey Chip USBKey Module"; + + public ScanGunKeyEventHelper(Context context, OnScanSuccessListener onScanSuccessListener) { + + mContext = context; + mOnScanSuccessListener = onScanSuccessListener; + mStringBufferResult = new StringBuffer(); + mHandler = new Handler(); + mScanningFishedRunnable = this::performScanSuccess; + } + + /** + * 返回扫码成功后的结果 + */ + private void performScanSuccess() { + String barcode = mStringBufferResult.toString(); + if (mOnScanSuccessListener != null && !TextUtils.isEmpty(barcode)) + mOnScanSuccessListener.onScanSuccess(barcode); + mStringBufferResult.setLength(0); + } + + /** + * 扫码枪事件解析 + * + * @param event + */ + public void analysisKeyEvent(KeyEvent event) { + int keyCode = event.getKeyCode(); + //字母大小写判断 + checkLetterStatus(event); + if (event.getAction() == KeyEvent.ACTION_DOWN) { + char aChar = getInputCode(event); + if (aChar != 0) { + mStringBufferResult.append(aChar); + } + if (keyCode == KeyEvent.KEYCODE_ENTER) { + //若为回车键,直接返回 + mHandler.removeCallbacks(mScanningFishedRunnable); + mHandler.post(mScanningFishedRunnable); + } else { + //延迟post,若500ms内,有其他事件 + mHandler.removeCallbacks(mScanningFishedRunnable); + mHandler.postDelayed(mScanningFishedRunnable, MESSAGE_DELAY); + } + + } + } + + //检查shift键 + private void checkLetterStatus(KeyEvent event) { + int keyCode = event.getKeyCode(); + if (keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT || keyCode == KeyEvent.KEYCODE_SHIFT_LEFT) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + //按着shift键,表示大写 + mCaps = true; + } else { + //松开shift键,表示小写 + mCaps = false; + } + } + } + + /** + * 获取扫描内容 + * + * @param event + * @return + */ + private char getInputCode(KeyEvent event) { + int keyCode = event.getKeyCode(); + char aChar; + if (keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z) { + //字母 + aChar = (char) ((mCaps ? 'A' : 'a') + keyCode - KeyEvent.KEYCODE_A); + } else if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) { + //数字 + aChar = (char) ('0' + keyCode - KeyEvent.KEYCODE_0); + } else { + //其他符号 + switch (keyCode) { + case KeyEvent.KEYCODE_PERIOD: + aChar = '.'; + break; + case KeyEvent.KEYCODE_MINUS: + aChar = mCaps ? '_' : '-'; + break; + case KeyEvent.KEYCODE_SLASH: + aChar = '/'; + break; + case KeyEvent.KEYCODE_BACKSLASH: + aChar = mCaps ? '|' : '\\'; + break; + default: + aChar = 0; + break; + } + } + return aChar; + } + + public interface OnScanSuccessListener { + void onScanSuccess(String barcode); + } + + public void onDestroy() { + mHandler.removeCallbacks(mScanningFishedRunnable); + mOnScanSuccessListener = null; + } + + /** + * 输入设备是否存在 + * + * @param deviceName + * @return + */ + public boolean isInputDeviceExist(String deviceName) { + + InputManager inputManager = (InputManager) mContext.getSystemService(INPUT_SERVICE); + int[] deviceIds = inputManager.getInputDeviceIds(); + for (int id : deviceIds) { + if (inputManager.getInputDevice(id).getName().equals(deviceName)) { + return true; + } + } + return false; + } + + /** + * 是否为扫码枪事件(部分机型KeyEvent获取的名字错误) + * + * @param event + * @return + */ + public boolean isScanGunEvent(KeyEvent event) { + if (event == null || event.getDevice() == null) return false; + String deviceName = event.getDevice().getName(); + L.e("event===" + deviceName + + "===Char===" + event.getCharacters() + + "===Action===" + event.getAction()); + return deviceName.equals(mDeviceName) || deviceName.equals(mDeviceName1); + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/ThreadUtils.kt b/lib_face/src/main/java/com/sw/plate/utils/ThreadUtils.kt new file mode 100644 index 0000000..caf51ed --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/ThreadUtils.kt @@ -0,0 +1,124 @@ +package com.sw.plate.utils + +import android.os.Handler +import android.os.Looper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import kotlin.coroutines.CoroutineContext + +/** + * 多功能线程工具类 + * 结合协程、Handler和线程池实现线程切换 + */ +object ThreadUtils : CoroutineScope { + // 主线程Handler + private val mainHandler by lazy { Handler(Looper.getMainLooper()) } + + // 后台线程池(IO密集型任务) + private val ioThreadPool: ExecutorService by lazy { + Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2) + } + + // CPU密集型线程池 + private val cpuThreadPool: ExecutorService by lazy { + Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()) + } + + // 协程Job管理 + private val job = Job() + + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main + job + + // ========== Handler相关方法 ========== + + /** + * 在主线程执行任务 + * @param delayMillis 延迟时间(毫秒) + */ + fun runOnUiThread(delayMillis: Long = 0, block: () -> Unit) { + if (delayMillis > 0) { + mainHandler.postDelayed(block, delayMillis) + } else { + if (isOnMainThread()) { + block() + } else { + mainHandler.post(block) + } + } + } + + /** + * 移除主线程任务 + */ + fun removeUiThreadTask(block: () -> Unit) { + mainHandler.removeCallbacks(block) + } + + // ========== 线程池相关方法 ========== + + /** + * 在IO线程执行任务 + */ + fun runOnIoThread(block: () -> Unit) { + ioThreadPool.execute(block) + } + + /** + * 在CPU计算线程执行任务 + */ + fun runOnCpuThread(block: () -> Unit) { + cpuThreadPool.execute(block) + } + + // ========== 协程相关方法 ========== + + /** + * 启动协程(默认在主线程) + */ + fun launch(block: suspend CoroutineScope.() -> Unit): Job { + return launch(coroutineContext, block = block) + } + + /** + * 在IO线程启动协程 + */ + fun launchOnIo(block: suspend CoroutineScope.() -> Unit): Job { + return launch(Dispatchers.IO, block = block) + } + + /** + * 切换到主线程(协程环境) + */ + suspend fun switchToMain(block: suspend CoroutineScope.() -> T): T { + return withContext(Dispatchers.Main, block) + } + + /** + * 切换到IO线程(协程环境) + */ + suspend fun switchToIo(block: suspend CoroutineScope.() -> T): T { + return withContext(Dispatchers.IO, block) + } + + /** + * 是否在主线程 + */ + fun isOnMainThread(): Boolean { + return Looper.myLooper() == Looper.getMainLooper() + } + + /** + * 释放资源 + */ + fun release() { + job.cancel() + ioThreadPool.shutdown() + cpuThreadPool.shutdown() + } +} \ No newline at end of file diff --git a/lib_face/src/main/java/com/sw/plate/utils/ToastUtils.java b/lib_face/src/main/java/com/sw/plate/utils/ToastUtils.java new file mode 100644 index 0000000..88bdcfc --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/ToastUtils.java @@ -0,0 +1,120 @@ +package com.sw.plate.utils; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import com.sw.plate.App; +import com.sw.plate.R; + +import java.util.Timer; +import java.util.TimerTask; + + +/** + * The type Toast utils. + */ +public class ToastUtils { + + private static Toast toast; + @SuppressLint("StaticFieldLeak") + private static TextView textCenterView; + + /** + * Show center toast. + * + * @param text the text + */ + public static void showToast(String text) { + if (TextUtils.isEmpty(text)) { + return; + } + runOnMainThread(()->{ + Context context = App.getContext(); + if (toast == null) { + View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null); + textCenterView = view.findViewById(R.id.toast_tv); + toast = new Toast(context); + toast.setGravity(Gravity.CENTER, 0, 20); + toast.setDuration(Toast.LENGTH_SHORT); + toast.setView(view); + } + textCenterView.setText(text); + toast.show(); + }); + } + + public static void runOnMainThread(Runnable runnable) { + new Handler(Looper.getMainLooper()).post(runnable); + } + + /** + * Show center toast. + * + * @param text the text + */ + public static void showToast(String text, int duration) { + runOnMainThread(()-> { + Context context = App.getContext(); + if (toast == null) { + View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null); + textCenterView = view.findViewById(R.id.toast_tv); + toast = new Toast(context); + toast.setGravity(Gravity.CENTER, 0, 20); + toast.setView(view); + } + + textCenterView.setText(text); + toast.setDuration(duration); + toast.show(); + }); + } + + public static void showLongToast(String text) { + runOnMainThread(()-> { + Context context = App.getContext(); + if (toast == null) { + View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null); + textCenterView = view.findViewById(R.id.toast_tv); + toast = new Toast(context); + toast.setGravity(Gravity.TOP, 0, 20); + toast.setDuration(Toast.LENGTH_LONG); + toast.setView(view); + } + textCenterView.setText(text); + showMyToast(toast, 1000000 * 30); + }); + } + + + //自定义Toast控件 + private static void showMyToast(final Toast toast, final int cnt) { + final Timer timer = new Timer(); + timer.schedule(new TimerTask() { + @Override + public void run() { + toast.show(); + } + }, 0, Toast.LENGTH_LONG); + new Timer().schedule(new TimerTask() { + @Override + public void run() { + toast.cancel(); + timer.cancel(); + } + }, cnt); + } + + public static void showSystemToast(String text) { + Context context = App.getContext(); + Toast.makeText(context, text, Toast.LENGTH_SHORT).show(); + } + +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/BindingUtil.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/BindingUtil.java new file mode 100644 index 0000000..8a74e0a --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/BindingUtil.java @@ -0,0 +1,42 @@ +package com.sw.plate.utils.arcface; + +import android.content.Context; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.sw.plate.utils.arcface.face.model.CompareResult; + +import java.text.SimpleDateFormat; +import java.util.List; + + +public class BindingUtil { + public static void setImagePath(ImageView imageView, String path) { + Glide.with(imageView.getContext()) + .load(path) + .into(imageView); + } + + public static void setCompareResultList(RecyclerView recyclerView, List compareResultList) { + Context context = recyclerView.getContext(); +// FaceSearchResultAdapter adapter = new FaceSearchResultAdapter(compareResultList, context); +// recyclerView.setAdapter(adapter); +// DisplayMetrics dm = context.getResources().getDisplayMetrics(); +// int spanCount = dm.widthPixels / +// (context.getResources().getDimensionPixelSize(R.dimen.item_head_image_padding) * 2 + +// context.getResources().getDimensionPixelSize(R.dimen.item_image_size)); +// recyclerView.setLayoutManager(new GridLayoutManager(context, spanCount)); +// recyclerView.setItemAnimator(new DefaultItemAnimator()); + } + + private static final SimpleDateFormat REGISTER_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); + + public static void setDate(TextView textView, long date) { + synchronized (REGISTER_DATE_FORMAT) { + textView.setText(REGISTER_DATE_FORMAT.format(date)); + } + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/ConfigUtil.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/ConfigUtil.java new file mode 100644 index 0000000..7a2299c --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/ConfigUtil.java @@ -0,0 +1,470 @@ +package com.sw.plate.utils.arcface; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import androidx.annotation.StringRes; + +import com.arcsoft.face.enums.DetectFaceOrientPriority; +import com.sw.plate.AppConst; +import com.sw.plate.R; + +/** + * 配置项设置,注意,{@link SharedPreferences}对象需要使用{@link PreferenceManager#getDefaultSharedPreferences(Context)}, + * 以确保和{@link androidx.preference.PreferenceFragmentCompat}操作同一个xml。 + */ +public class ConfigUtil { + /** + * 识别阈值 + */ + private static final float RECOMMEND_RECOGNIZE_THRESHOLD = 0.80f; + /** + * 遮挡阈值 + */ + private static final float RECOMMEND_SHELTER_THRESHOLD = 0.50f; + /** + * 眼睛开启阈值 + */ + private static final float RECOMMEND_EYE_OPEN_THRESHOLD = 0.50f; + /** + * 嘴巴闭合阈值 + */ + private static final float RECOMMEND_MOUTH_CLOSE_THRESHOLD = 0.50f; + /** + * 戴眼镜阈值 + */ + private static final float RECOMMEND_WEAR_GLASSES_THRESHOLD = 0.50f; + /** + * 可见光活体检测阈值 + */ + private static final float RECOMMEND_RGB_LIVENESS_THRESHOLD = 0.50f; + /** + * 红外活体检测阈值 + */ + private static final float RECOMMEND_IR_LIVENESS_THRESHOLD = 0.70f; + /** + * 活体 FQ 检测阈值 + */ + private static final float RECOMMEND_LIVENESS_FQ_THRESHOLD = 0.65f; + /** + * 可见光活体模型选择界限 + */ + private static final int RECOMMEND_RGB_LIVENESS_FACE_SIZE_THRESHOLD = 80; + /** + * 可见光活体模型选择界限 + */ + private static final int RECOMMEND_IR_LIVENESS_FACE_SIZE_THRESHOLD = 90; + /** + * 图像质量检测阈值:未戴口罩,且在人脸识别场景下 + */ + public static final float IMAGE_QUALITY_NO_MASK_RECOGNIZE_THRESHOLD = 0.49f; + /** + * 图像质量检测阈值:未戴口罩,且在人脸注册场景下 + */ + public static final float IMAGE_QUALITY_NO_MASK_REGISTER_THRESHOLD = 0.63f; + /** + * 图像质量检测阈值:戴口罩,且在人脸识别场景下 + */ + public static final float IMAGE_QUALITY_MASK_RECOGNIZE_THRESHOLD = 0.29f; + + /** + * 人脸大小限制 + */ + private static final int RECOMMEND_FACE_SIZE_LIMIT = 360; + /** + * 上下帧人脸移动像素数限制 + */ + private static final int RECOMMEND_FACE_MOVE_LIMIT = 20; + /** + * 默认最大人脸检测数量 + */ + private static final int DEFAULT_MAX_DETECT_FACE_NUM = 1; + /** + * 默认人脸大小占比 + */ + private static final int DEFAULT_SCALE = 16; + /** + * 默认相机分辨率 + */ +// private static final String DEFAULT_PREVIEW_SIZE = "1280x720"; +// private static final String DEFAULT_PREVIEW_SIZE = "1080x720"; +// private static final String DEFAULT_PREVIEW_SIZE = "720x1080"; +// private static final String DEFAULT_PREVIEW_SIZE = "720x1280"; +// private static final String DEFAULT_PREVIEW_SIZE = "1024x768"; + private static final String DEFAULT_PREVIEW_SIZE = "1024x768"; +// private static final String DEFAULT_PREVIEW_SIZE = "800x600"; +// private static final String DEFAULT_PREVIEW_SIZE = "640x480"; +// private static final String DEFAULT_PREVIEW_SIZE = "640x480"; + + + /** + * 获取String类型的preference + * + * @param context 上下文 + * @param keyRes key的Id + * @param defaultValue 默认值 + * @return preference值 + */ + private static String getString(Context context, @StringRes int keyRes, String defaultValue) { + if (context == null) { + return defaultValue; + } + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + String key = context.getString(keyRes); + return sharedPreferences.getString(key, defaultValue); + } + + /** + * 获取boolean类型的preference + * + * @param context 上下文 + * @param keyRes key的Id + * @param defaultValue 默认值 + * @return preference值 + */ + private static boolean getBoolean(Context context, @StringRes int keyRes, boolean defaultValue) { + if (context == null) { + return defaultValue; + } + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + String key = context.getString(keyRes); + return sharedPreferences.getBoolean(key, defaultValue); + } + + /** + * 获取int类型的preference + * + * @param context 上下文 + * @param keyRes key的Id + * @param defaultValue 默认值 + * @return preference值 + */ + private static int getInt(Context context, @StringRes int keyRes, int defaultValue) { + if (context == null) { + return defaultValue; + } + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + String key = context.getString(keyRes); + return sharedPreferences.getInt(key, defaultValue); + } + + /** + * 获取float类型的preference + * + * @param context 上下文 + * @param keyRes key的Id + * @param defaultValue 默认值 + * @return preference值 + */ + private static float getFloat(Context context, @StringRes int keyRes, float defaultValue) { + if (context == null) { + return defaultValue; + } + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + String key = context.getString(keyRes); + return sharedPreferences.getFloat(key, defaultValue); + } + + /** + * 保存int类型的preference + * + * @param context 上下文 + * @param keyRes key的Id + * @param newValue key对应的value + * @return 是否保存成功 + */ + private static boolean commitInt(Context context, @StringRes int keyRes, int newValue) { + if (context == null) { + return false; + } + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + return sharedPreferences.edit() + .putInt(context.getString(keyRes), newValue) + .commit(); + } + + /** + * 保存String类型的preference + * + * @param context 上下文 + * @param keyRes key的Id + * @param newValue key对应的value + * @return 是否保存成功 + */ + private static boolean commitString(Context context, @StringRes int keyRes, String newValue) { + if (context == null) { + return false; + } + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + return sharedPreferences.edit() + .putString(context.getString(keyRes), newValue) + .commit(); + } + + /** + * 设置截至目前已track到的人脸数 + * + * @param context 上下文 + * @param trackedFaceCount 截至目前已track到的人脸数 + * @return 是否保存成功 + */ + public static boolean setTrackedFaceCount(Context context, int trackedFaceCount) { + if (context == null) { + return false; + } + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + return sharedPreferences.edit() + .putInt(context.getString(R.string.preference_track_face_count), trackedFaceCount) + .commit(); + } + + /** + * 获取到截至目前已track到的人脸数 + * + * @param context 上下文 + * @return 之前已track到的人脸数 + */ + public static int getTrackedFaceCount(Context context) { + return getInt(context, R.string.preference_track_face_count, 0); + } + + /** + * 获取VIDEO模式人脸检测角度优先级 + * + * @param context 上下文 + * @return VIDEO模式人脸检测角度优先级 + */ + public static DetectFaceOrientPriority getFtOrient(Context context) { + if (context == null) { + return DetectFaceOrientPriority.ASF_OP_ALL_OUT; + } + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + return DetectFaceOrientPriority.valueOf(sharedPreferences.getString(context.getString(R.string.preference_choose_detect_degree), DetectFaceOrientPriority.ASF_OP_ALL_OUT.name())); + } + + /** + * TODO: 该Demo基于单人脸识别实现,若想使用多人脸识别,请将 return true 改成 return getBoolean,并修改相关配置项的preference.xml和业务代码 + *

+ * 获取识别界面是否保留最大人脸 + * + * @param context 上下文 + * @return 别界面是否保留最大人脸 + */ + public static boolean isKeepMaxFace(Context context) { +// return getBoolean(context, R.string.preference_recognize_keep_max_face, false); + return true; + } + + /** + * 获取是否限制人脸识别区域 + * + * @param context 上下文 + * @return 是否限制人脸识别区域 + */ + public static boolean isRecognizeAreaLimited(Context context) { + return getBoolean(context, R.string.preference_recognize_limit_recognize_area, false); + } + + /** + * 视频人脸比对界面中,获取最大的人脸检测数量 + * + * @param context 上下文 + * @return 最大的人脸检测数量 + */ + public static int getRecognizeMaxDetectFaceNum(Context context) { + try { + return Integer.parseInt(getString(context, R.string.preference_recognize_max_detect_num, String.valueOf(DEFAULT_MAX_DETECT_FACE_NUM))); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + return DEFAULT_MAX_DETECT_FACE_NUM; + } + + /** + * 视频人脸比对界面中,获取预先设置的scale值 + * + * @param context 上下文 + * @return scale值 + */ + public static int getRecognizeScale(Context context) { + try { + return Integer.parseInt(getString(context, R.string.preference_recognize_scale_value, String.valueOf(DEFAULT_SCALE))); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + return DEFAULT_SCALE; + } + + /** + * 获取双目水平成像偏移量 + * + * @param context 上下文 + * @return 双目水平偏移量 + */ + public static int getDualCameraHorizontalOffset(Context context) { + return getInt(context, R.string.preference_dual_camera_offset_horizontal, 0); + } + + /** + * 获取双目垂直成像偏移量 + * + * @param context 上下文 + * @return 双目水平偏移量 + */ + public static int getDualCameraVerticalOffset(Context context) { + return getInt(context, R.string.preference_dual_camera_offset_vertical, 0); + } + + /** + * 视频人脸比对界面中,获取预先设置的识别阈值 + * + * @param context 上下文 + * @return 识别阈值 + */ + public static float getRecognizeThreshold(Context context) { + return Float.parseFloat(getString(context, R.string.preference_recognize_threshold, String.valueOf(RECOMMEND_RECOGNIZE_THRESHOLD))); + } + + public static float getRecognizeShelterThreshold(Context context) { + return Float.parseFloat(getString(context, R.string.preference_shelter_threshold, String.valueOf(RECOMMEND_SHELTER_THRESHOLD))); + } + + public static float getRecognizeEyeOpenThreshold(Context context) { + return Float.parseFloat(getString(context, R.string.preference_eye_open_threshold, String.valueOf(RECOMMEND_EYE_OPEN_THRESHOLD))); + } + + public static float getRecognizeMouthCloseThreshold(Context context) { + return Float.parseFloat(getString(context, R.string.preference_mouth_close_threshold, String.valueOf(RECOMMEND_MOUTH_CLOSE_THRESHOLD))); + } + + public static float getRecognizeWearGlassesThreshold(Context context) { + return Float.parseFloat(getString(context, R.string.preference_wear_glasses_threshold, String.valueOf(RECOMMEND_WEAR_GLASSES_THRESHOLD))); + } + + public static float getRgbLivenessThreshold(Context context) { + return Float.parseFloat(getString(context, R.string.preference_rgb_liveness_threshold, String.valueOf(RECOMMEND_RGB_LIVENESS_THRESHOLD))); + } + + public static float getIrLivenessThreshold(Context context) { + return Float.parseFloat(getString(context, R.string.preference_ir_liveness_threshold, String.valueOf(RECOMMEND_IR_LIVENESS_THRESHOLD))); + } + + public static float getLivenessFqThreshold(Context context) { + return Float.parseFloat(getString(context, R.string.preference_liveness_fq_threshold, String.valueOf(RECOMMEND_LIVENESS_FQ_THRESHOLD))); + } + + public static int getRgbLivenessFaceSizeThreshold(Context context) { + return Integer.parseInt(getString(context, R.string.preference_rgb_liveness_face_size_threshold, String.valueOf(RECOMMEND_RGB_LIVENESS_FACE_SIZE_THRESHOLD))); + } + + public static int getIrLivenessFaceSizeThreshold(Context context) { + return Integer.parseInt(getString(context, R.string.preference_ir_liveness_face_size_threshold, String.valueOf(RECOMMEND_IR_LIVENESS_FACE_SIZE_THRESHOLD))); + } + + public static float getImageQualityNoMaskRecognizeThreshold(Context context) { + return Float.parseFloat(getString(context, R.string.preference_image_quality_no_mask_recognize_threshold, + String.valueOf(IMAGE_QUALITY_NO_MASK_RECOGNIZE_THRESHOLD))); + } + + public static float getImageQualityNoMaskRegisterThreshold(Context context) { + return Float.parseFloat(getString(context, R.string.preference_image_quality_no_mask_register_threshold, + String.valueOf(IMAGE_QUALITY_NO_MASK_REGISTER_THRESHOLD))); + } + + public static float getImageQualityMaskRecognizeThreshold(Context context) { + return Float.parseFloat(getString(context, R.string.preference_image_quality_mask_recognize_threshold, + String.valueOf(IMAGE_QUALITY_MASK_RECOGNIZE_THRESHOLD))); + } + + public static int getFaceSizeLimit(Context context) { + return Integer.parseInt(getString(context, R.string.preference_recognize_face_size_limit, String.valueOf(RECOMMEND_FACE_SIZE_LIMIT))); + } + + public static int getFaceMoveLimit(Context context) { + return Integer.parseInt(getString(context, R.string.preference_recognize_move_pixel_limit, String.valueOf(RECOMMEND_FACE_MOVE_LIMIT))); + } + + public static String getLivenessDetectType(Context context) { + return getString(context, R.string.preference_liveness_detect_type, context.getString(R.string.value_liveness_type_rgb)); + } + + + public static boolean isEnableImageQualityDetect(Context context) { + return getBoolean(context, R.string.preference_enable_image_quality_detect, true); + } + + public static boolean isEnableFaceSizeLimit(Context context) { + return getBoolean(context, R.string.preference_enable_face_size_limit, false); + } + + public static boolean isEnableFaceMoveLimit(Context context) { + return getBoolean(context, R.string.preference_enable_face_move_limit, false); + } + + public static boolean isSwitchCamera(Context context) { + return getBoolean(context, R.string.preference_switch_camera, false); + } + + public static String getPreviewSize(Context context) { + return getString(context, R.string.preference_dual_camera_preview_size, DEFAULT_PREVIEW_SIZE); + } + + public static String getRgbCameraAdditionalRotation(Context context) { + return getString(context, R.string.preference_rgb_camera_rotation, "0"); + } + + public static String getIrCameraAdditionalRotation(Context context) { + return getString(context, R.string.preference_ir_camera_rotation, "0"); + } + + public static String getAppId(Context context) { + return getString(context, R.string.preference_app_id, AppConst.ARCSOFT_APP_ID); + } + + public static String getSdkKey(Context context) { + return getString(context, R.string.preference_sdk_key, AppConst.ARCSOFT_SDK_KEY); + } + + public static String getActiveKey(Context context) { + return getString(context, R.string.preference_active_key, AppConst.ARCSOFT_ACTIVE_KEY); + } + + public static boolean commitAppId(Context context, String appId) { + return commitString(context, R.string.preference_app_id, appId); + } + + public static boolean commitSdkKey(Context context, String sdkKey) { + return commitString(context, R.string.preference_sdk_key, sdkKey); + } + + public static boolean commitActiveKey(Context context, String activeKey) { + return commitString(context, R.string.preference_active_key, activeKey); + } + + + public static boolean isDrawRgbRectHorizontalMirror(Context context) { + return getBoolean(context, R.string.preference_draw_rgb_rect_horizontal_mirror, false); + } + + public static boolean isDrawIrRectHorizontalMirror(Context context) { + return getBoolean(context, R.string.preference_draw_ir_rect_horizontal_mirror, false); + } + + public static boolean isDrawRgbRectVerticalMirror(Context context) { + return getBoolean(context, R.string.preference_draw_rgb_rect_vertical_mirror, false); + } + + public static boolean isDrawIrRectVerticalMirror(Context context) { + return getBoolean(context, R.string.preference_draw_ir_rect_vertical_mirror, false); + } + + public static boolean isDrawRgbPreviewHorizontalMirror(Context context) { + return getBoolean(context, R.string.preference_rgb_preview_horizontal_mirror, false); + } + + public static boolean isDrawIrPreviewHorizontalMirror(Context context) { + return getBoolean(context, R.string.preference_ir_preview_horizontal_mirror, false); + } + +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/ErrorCodeUtil.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/ErrorCodeUtil.java new file mode 100644 index 0000000..c8ba88a --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/ErrorCodeUtil.java @@ -0,0 +1,51 @@ +package com.sw.plate.utils.arcface; + +import com.arcsoft.face.ErrorInfo; +import com.arcsoft.imageutil.ArcSoftImageUtilError; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +public class ErrorCodeUtil { + /** + * 将ArcFace错误码转换为对应的错误码常量名,便于理解 + * TODO:目前每次都遍历,如果使用频繁,建议将Field缓存处理,避免每次都反射 + * + * @param code 错误码 + * @return 错误码常量名 + */ + public static String arcFaceErrorCodeToFieldName(int code) { + Field[] declaredFields = ErrorInfo.class.getDeclaredFields(); + for (Field declaredField : declaredFields) { + try { + if (Modifier.isFinal(declaredField.getModifiers()) && ((int) declaredField.get(ErrorInfo.class)) == code) { + return declaredField.getName(); + } + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + return "unknown error"; + } + + /** + * 将ArcSoftImageUtil错误码转换为对应的错误码常量名,便于理解 + * TODO:目前每次都遍历,如果使用频繁,建议将Field缓存处理,避免每次都反射 + * + * @param code 错误码 + * @return 错误码常量名 + */ + public static String imageUtilErrorCodeToFieldName(int code) { + Field[] declaredFields = ArcSoftImageUtilError.class.getDeclaredFields(); + for (Field declaredField : declaredFields) { + try { + if (Modifier.isFinal(declaredField.getModifiers()) && ((int) declaredField.get(ArcSoftImageUtilError.class)) == code) { + return declaredField.getName(); + } + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + return "unknown error"; + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceApi.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceApi.java new file mode 100644 index 0000000..4b3753e --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceApi.java @@ -0,0 +1,95 @@ +package com.sw.plate.utils.arcface; + +import android.content.Context; +import android.util.Log; + +import com.arcsoft.face.FaceEngine; +import com.arcsoft.face.enums.RuntimeABI; +import com.sw.plate.App; +import com.sw.plate.utils.L; +import com.sw.plate.utils.arcface.face.model.FacePreviewInfo; +import com.sw.plate.utils.arcface.facedb.FaceDatabase; +import com.sw.plate.utils.arcface.facedb.dao.FaceDao; +import com.sw.plate.utils.arcface.facedb.entity.FaceEntity; + +import java.util.List; + +public class FaceApi { + private static final String TAG = "FaceApi"; + + public interface ActiveCallback { + void onSuccess(int code); + + void onFail(Exception e); + } + + /** + * 更新人脸数据 + * + * @param index + * @param list + */ + public void updateFaceData(int index, List list) { + Log.d(TAG, "updateFaceData: index = " + index + ", listSize = " + list.size()); + FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao(); + if (index == 1) { + faceDao.deleteAll(); + faceDao.resetId(); + } + List insertIdList = faceDao.insert(list); + Log.d(TAG, "updateFaceData: count = " + insertIdList.size()); + List queryList = faceDao.getAllFaces(); + Log.d(TAG, "updateFaceData: queryList.size = " + queryList.size()); + } + + public void updateFaceData2(int index, List list) { + Log.d(TAG, "updateFaceData: index = " + index + ", listSize = " + list.size()); + FaceDao faceDao = FaceDatabase.getInstance(App.getContext()).faceDao(); + if (index == 0) { + faceDao.deleteAll(); + faceDao.resetId(); + } + faceDao.insert(list); + } + + /** + * 激活arcsoft 人脸 + * + * @param context + * @param arcsoftAppId + * @param arcsoftSdkKey + * @param arcsoftActiveKey + * @param callback + */ + public void activeEngine(Context context, String arcsoftAppId, + String arcsoftSdkKey, + String arcsoftActiveKey, ActiveCallback callback) { + new Thread(() -> { + try { + RuntimeABI runtimeABI = FaceEngine.getRuntimeABI(); + L.e("subscribe: getRuntimeABI() " + runtimeABI); + + long start = System.currentTimeMillis(); + int activeCode = FaceEngine.activeOnline(context, arcsoftActiveKey, + arcsoftAppId, arcsoftSdkKey); + L.e("subscribe cost: " + (System.currentTimeMillis() - start)); + callback.onSuccess(activeCode); + } catch (Exception e) { + callback.onFail(e); + } + }).start(); + } + + /** + * 传入可见光相机预览数据 + * + * @param nv21 可见光相机预览数据 + * @param doRecognize 是否进行识别 + * @return 当前帧的检测结果信息 + */ + public List onPreviewFrame(byte[] nv21, boolean doRecognize) { + +// List facePreviewInfoList = recognizeViewModel.onPreviewFrame(nv21, true); + return null; + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectTransformer.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectTransformer.java new file mode 100644 index 0000000..7b34137 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectTransformer.java @@ -0,0 +1,238 @@ +package com.sw.plate.utils.arcface; + +import android.graphics.Rect; +import android.hardware.Camera; + +import com.sw.plate.utils.L; + +/** + * 将检测回传的人脸框(基于NV21数据)转换为View绘制(基于View)所需的人脸框 + */ +public class FaceRectTransformer { + private int previewWidth, previewHeight, canvasWidth, canvasHeight, cameraDisplayOrientation, cameraId; + private boolean isMirror; + private boolean mirrorHorizontal = false, mirrorVertical = false; + + /** + * 创建一个绘制辅助类对象,并且设置绘制相关的参数 + * + * @param previewWidth 预览宽度 + * @param previewHeight 预览高度 + * @param canvasWidth 绘制控件的宽度 + * @param canvasHeight 绘制控件的高度 + * @param cameraDisplayOrientation 旋转角度 + * @param cameraId 相机ID + * @param isMirror 是否水平镜像显示(若相机是镜像显示的,设为true,用于纠正) + * @param mirrorHorizontal 为兼容部分设备使用,水平再次镜像 + * @param mirrorVertical 为兼容部分设备使用,垂直再次镜像 + */ + public FaceRectTransformer(int previewWidth, int previewHeight, int canvasWidth, + int canvasHeight, int cameraDisplayOrientation, int cameraId, + boolean isMirror, boolean mirrorHorizontal, boolean mirrorVertical) { + this.previewWidth = previewWidth; + this.previewHeight = previewHeight; + this.canvasWidth = canvasWidth; + this.canvasHeight = canvasHeight; + this.cameraDisplayOrientation = cameraDisplayOrientation; + this.cameraId = cameraId; + this.isMirror = isMirror; + this.mirrorHorizontal = mirrorHorizontal; + this.mirrorVertical = mirrorVertical; + } + + /** + * 调整人脸框用来绘制 + * + * @param ftRect FT人脸框 + * @return 调整后的需要被绘制到View上的rect + */ + public Rect adjustRect(Rect ftRect) { + int previewWidth = this.previewWidth; + int previewHeight = this.previewHeight; + int canvasWidth = this.canvasWidth; + int canvasHeight = this.canvasHeight; + int cameraDisplayOrientation = this.cameraDisplayOrientation; + int cameraId = this.cameraId; + boolean isMirror = this.isMirror; + boolean mirrorHorizontal = this.mirrorHorizontal; + boolean mirrorVertical = this.mirrorVertical; + + if (ftRect == null) { + return null; + } + + Rect rect = new Rect(ftRect); + float horizontalRatio; + float verticalRatio; + if (cameraDisplayOrientation % 180 == 0) { + horizontalRatio = (float) canvasWidth / (float) previewWidth; + verticalRatio = (float) canvasHeight / (float) previewHeight; + } else { + horizontalRatio = (float) canvasHeight / (float) previewWidth; + verticalRatio = (float) canvasWidth / (float) previewHeight; + } + rect.left *= horizontalRatio; + rect.right *= horizontalRatio; + rect.top *= verticalRatio; + rect.bottom *= verticalRatio; + + Rect newRect = new Rect(); +// L.e("cameraDisplayOrientation " + cameraDisplayOrientation + " === " + cameraId); + switch (cameraDisplayOrientation) { + case 0: + if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) { + newRect.left = canvasWidth - rect.right; + newRect.right = canvasWidth - rect.left; + +// newRect.left = rect.left; +// newRect.right = rect.right; + } else { + newRect.left = rect.left; + newRect.right = rect.right; + +// newRect.left = canvasWidth - rect.right; +// newRect.right = canvasWidth - rect.left; + } + newRect.top = rect.top; + newRect.bottom = rect.bottom; + break; + case 90: + newRect.right = canvasWidth - rect.top; + newRect.left = canvasWidth - rect.bottom; + if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) { + newRect.top = canvasHeight - rect.right; + newRect.bottom = canvasHeight - rect.left; + } else { + newRect.top = rect.left; + newRect.bottom = rect.right; + } + break; + case 180: + newRect.top = canvasHeight - rect.bottom; + newRect.bottom = canvasHeight - rect.top; + if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) { + newRect.left = rect.left; + newRect.right = rect.right; + } else { + newRect.left = canvasWidth - rect.right; + newRect.right = canvasWidth - rect.left; + +// newRect.left = rect.left; +// newRect.right = rect.right; + } + break; + case 270: +// newRect.left = rect.top; +// newRect.right = rect.bottom; + + newRect.left = canvasWidth - rect.right; + newRect.right = canvasWidth - rect.left; + + if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) { + newRect.top = canvasHeight - rect.right; + newRect.bottom = canvasHeight - rect.left; + } else { + newRect.top = rect.left; + newRect.bottom = rect.right; + } + break; + default: + break; + } + + /** + * isMirror mirrorHorizontal finalIsMirrorHorizontal + * true true false + * false false false + * true false true + * false true true + * + * XOR + */ + if (isMirror ^ mirrorHorizontal) { + int left = newRect.left; + int right = newRect.right; + newRect.left = canvasWidth - right; + newRect.right = canvasWidth - left; + } + if (mirrorVertical) { + int top = newRect.top; + int bottom = newRect.bottom; + newRect.top = canvasHeight - bottom; + newRect.bottom = canvasHeight - top; + } + return newRect; + } + + public void setPreviewWidth(int previewWidth) { + this.previewWidth = previewWidth; + } + + public void setPreviewHeight(int previewHeight) { + this.previewHeight = previewHeight; + } + + public void setCanvasWidth(int canvasWidth) { + this.canvasWidth = canvasWidth; + } + + public void setCanvasHeight(int canvasHeight) { + this.canvasHeight = canvasHeight; + } + + public void setCameraDisplayOrientation(int cameraDisplayOrientation) { + this.cameraDisplayOrientation = cameraDisplayOrientation; + } + + public void setCameraId(int cameraId) { + this.cameraId = cameraId; + } + + public void setMirror(boolean mirror) { + isMirror = mirror; + } + + public int getPreviewWidth() { + return previewWidth; + } + + public int getPreviewHeight() { + return previewHeight; + } + + public int getCanvasWidth() { + return canvasWidth; + } + + public int getCanvasHeight() { + return canvasHeight; + } + + public int getCameraDisplayOrientation() { + return cameraDisplayOrientation; + } + + public int getCameraId() { + return cameraId; + } + + public boolean isMirror() { + return isMirror; + } + + public boolean isMirrorHorizontal() { + return mirrorHorizontal; + } + + public void setMirrorHorizontal(boolean mirrorHorizontal) { + this.mirrorHorizontal = mirrorHorizontal; + } + + public boolean isMirrorVertical() { + return mirrorVertical; + } + + public void setMirrorVertical(boolean mirrorVertical) { + this.mirrorVertical = mirrorVertical; + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectView.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectView.java new file mode 100644 index 0000000..c58ba6e --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectView.java @@ -0,0 +1,298 @@ +package com.sw.plate.utils.arcface; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; + +import androidx.annotation.Nullable; + +import com.arcsoft.face.FaceAttributeInfo; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * 用于显示人脸信息的控件 + */ +public class FaceRectView extends View { + private CopyOnWriteArrayList drawInfoList = new CopyOnWriteArrayList<>(); + + // 画笔,复用 + private Paint paint; + + // 默认人脸框厚度 + private static final int DEFAULT_FACE_RECT_THICKNESS = 6; + + public FaceRectView(Context context) { + this(context, null); + } + + public FaceRectView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + paint = new Paint(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (drawInfoList != null && drawInfoList.size() > 0) { + for (int i = 0; i < drawInfoList.size(); i++) { + drawFaceRect(canvas, drawInfoList.get(i), DEFAULT_FACE_RECT_THICKNESS, paint); + } + } + } + + public void clearFaceInfo() { + drawInfoList.clear(); + postInvalidate(); + } + + public void addFaceInfo(DrawInfo faceInfo) { + drawInfoList.add(faceInfo); + postInvalidate(); + } + + public void addFaceInfo(List faceInfoList) { + drawInfoList.addAll(faceInfoList); + postInvalidate(); + } + + public void drawRealtimeFaceInfo(List drawInfoList) { + clearFaceInfo(); + if (drawInfoList == null || drawInfoList.size() == 0) { + return; + } + addFaceInfo(drawInfoList); + } + + public static class DrawInfo { + private Rect rect; + private int sex; + private int age; + private int liveness; + private int color; + private int isWithinBoundary; + private String name = null; + private boolean drawRectInfo; + private Rect foreheadRect; + private FaceAttributeInfo faceAttributeInfo; + private boolean rgbRect; + + public DrawInfo(Rect rect, int sex, int age, int liveness, int color, String name) { + this.rect = rect; + this.sex = sex; + this.age = age; + this.liveness = liveness; + this.color = color; + this.name = name; + } + + public DrawInfo(Rect rect, int sex, int age, int liveness, int color, String name, int isWithinBoundary, Rect foreheadRect, + FaceAttributeInfo faceAttributeInfo, boolean drawRectInfo, boolean rgbRect) { + this.rect = rect; + this.sex = sex; + this.age = age; + this.liveness = liveness; + this.color = color; + this.name = name; + this.isWithinBoundary = isWithinBoundary; + this.drawRectInfo = drawRectInfo; + this.foreheadRect = foreheadRect; + this.faceAttributeInfo = faceAttributeInfo; + this.rgbRect = rgbRect; + } + + public DrawInfo(DrawInfo drawInfo) { + if (drawInfo == null) { + return; + } + this.rect = drawInfo.rect; + this.sex = drawInfo.sex; + this.age = drawInfo.age; + this.liveness = drawInfo.liveness; + this.color = drawInfo.color; + this.name = drawInfo.name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Rect getRect() { + return rect; + } + + public void setRect(Rect rect) { + this.rect = rect; + } + + public int getSex() { + return sex; + } + + public void setSex(int sex) { + this.sex = sex; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public int getLiveness() { + return liveness; + } + + public void setLiveness(int liveness) { + this.liveness = liveness; + } + + public int getColor() { + return color; + } + + public void setColor(int color) { + this.color = color; + } + + public boolean isDrawRectInfo() { + return drawRectInfo; + } + + public void setDrawRectInfo(boolean drawRectInfo) { + this.drawRectInfo = drawRectInfo; + } + + public Rect getForeheadRect() { + return foreheadRect; + } + + public void setForeheadRect(Rect foreheadRect) { + this.foreheadRect = foreheadRect; + } + + public FaceAttributeInfo getFaceAttributeInfo() { + return faceAttributeInfo; + } + + public void setFaceAttributeInfo(FaceAttributeInfo faceAttributeInfo) { + this.faceAttributeInfo = faceAttributeInfo; + } + + public int getIsWithinBoundary() { + return isWithinBoundary; + } + + public void setIsWithinBoundary(int isWithinBoundary) { + this.isWithinBoundary = isWithinBoundary; + } + } + + + /** + * 绘制数据信息到view上,若 {@link DrawInfo#getName()} 不为null则绘制 {@link DrawInfo#getName()} + * + * @param canvas 需要被绘制的view的canvas + * @param drawInfo 绘制信息 + * @param faceRectThickness 人脸框厚度 + * @param paint 画笔 + */ + private static void drawFaceRect(Canvas canvas, DrawInfo drawInfo, int faceRectThickness, Paint paint) { + if (canvas == null || drawInfo == null) { + return; + } + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(faceRectThickness); + paint.setColor(drawInfo.getColor()); +// paint.setColor(Color.parseColor("#FF0000")); + paint.setAntiAlias(true); + + Path mPath = new Path(); + // 左上 + Rect rect = drawInfo.getRect(); + mPath.moveTo(rect.left, rect.top + rect.height() / 4); + mPath.lineTo(rect.left, rect.top); + mPath.lineTo(rect.left + rect.width() / 4, rect.top); + // 右上 + mPath.moveTo(rect.right - rect.width() / 4, rect.top); + mPath.lineTo(rect.right, rect.top); + mPath.lineTo(rect.right, rect.top + rect.height() / 4); + // 右下 + mPath.moveTo(rect.right, rect.bottom - rect.height() / 4); + mPath.lineTo(rect.right, rect.bottom); + mPath.lineTo(rect.right - rect.width() / 4, rect.bottom); + // 左下 + mPath.moveTo(rect.left + rect.width() / 4, rect.bottom); + mPath.lineTo(rect.left, rect.bottom); + mPath.lineTo(rect.left, rect.bottom - rect.height() / 4); + canvas.drawPath(mPath, paint); + + // 绘制文字,用最细的即可,避免在某些低像素设备上文字模糊 +// paint.setStrokeWidth(1); +// +// if (drawInfo.getName() == null) { +// paint.setStyle(Paint.Style.FILL_AND_STROKE); +// paint.setTextSize(rect.width() / 12); +// String str = (drawInfo.getSex() == GenderInfo.MALE ? "MALE" : (drawInfo.getSex() == GenderInfo.FEMALE ? "FEMALE" : "UNKNOWN")) +// + "," +// + (drawInfo.getAge() == AgeInfo.UNKNOWN_AGE ? "UNKNOWN" : drawInfo.getAge()) +// + "," +// + (drawInfo.getLiveness() == LivenessInfo.ALIVE ? "ALIVE" : (drawInfo.getLiveness() == LivenessInfo.NOT_ALIVE ? "NOT_ALIVE" : "UNKNOWN")); +// canvas.drawText(str, rect.left, rect.top - 10, paint); +// } else { +// paint.setStyle(Paint.Style.FILL_AND_STROKE); +// paint.setTextSize(rect.width() / 12); +// canvas.drawText(drawInfo.getName(), rect.left, rect.top - 10, paint); +// } + +// if (drawInfo.drawRectInfo && drawInfo.rgbRect) { +// Rect foreRect = drawInfo.foreheadRect; +// if (foreRect != null) { +// Path forePath = new Path(); +// forePath.moveTo(foreRect.left, foreRect.top); +// forePath.lineTo(foreRect.right, foreRect.top); +// forePath.lineTo(foreRect.right, foreRect.bottom); +// forePath.lineTo(foreRect.left, foreRect.bottom); +// forePath.lineTo(foreRect.left, foreRect.top); +// paint.setStyle(Paint.Style.STROKE); +// paint.setStrokeWidth(3); +// canvas.drawPath(forePath, paint); +// } +// +// FaceAttributeInfo attributeInfo = drawInfo.getFaceAttributeInfo(); +// if (attributeInfo != null) { +// paint.setStyle(Paint.Style.FILL_AND_STROKE); +// int textSize = rect.width() / 8; +// paint.setStrokeWidth(1); +// paint.setTextSize(textSize); +// int defX = rect.left; +// int defY = rect.bottom + rect.width() / 8; +// +// String strInfo0 = "isWithinBoundary: " + drawInfo.getIsWithinBoundary(); +// canvas.drawText(strInfo0, defX, defY, paint); +// +// String strInfo1 = "WearGlasses: " + attributeInfo.getWearGlasses(); +// canvas.drawText(strInfo1, defX, defY + textSize, paint); +// +// String strInfo2 = "EyeOpen: [" + attributeInfo.getLeftEyeOpen() + "," + attributeInfo.getRightEyeOpen() + "]"; +// canvas.drawText(strInfo2, rect.left, defY + textSize * 2, paint); +// +// String strInfo3 = "MouseClose: " + attributeInfo.getMouthClose(); +// canvas.drawText(strInfo3, rect.left, defY + textSize * 3, paint); +// } +// } + } + +} \ No newline at end of file diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/FileUtil.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/FileUtil.java new file mode 100644 index 0000000..e0efc7e --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/FileUtil.java @@ -0,0 +1,71 @@ +package com.sw.plate.utils.arcface; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +public class FileUtil { + /** + * 读取文件中的数据内容 + * + * @param file 文件 + * @return 二进制数据内容 + */ + public static byte[] fileToData(File file) { + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + byte[] data = new byte[fis.available()]; + fis.read(data); + fis.close(); + return data; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + public static boolean saveDataToFile(byte[] data, File file, boolean append) { + if (data == null) { + return false; + } + File parentFile = file.getParentFile(); + if (parentFile == null) { + return false; + } + if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) { + return false; + } + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file, append); + int bufferSize = 1024; + int index = 0; + while (index < data.length) { + if (data.length - index < bufferSize) { + bufferSize = data.length - index; + } + fos.write(data, index, bufferSize); + index += bufferSize; + } + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } finally { + try { + if (fos != null) { + fos.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public static boolean saveDataToFile(byte[] data, File file) { + return saveDataToFile(data, file, false); + } + +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/ImageUtil.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/ImageUtil.java new file mode 100644 index 0000000..0c8fcdd --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/ImageUtil.java @@ -0,0 +1,229 @@ +package com.sw.plate.utils.arcface; + +import android.content.ContentResolver; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Rect; +import android.net.Uri; + +import java.io.IOException; +import java.io.InputStream; + +public class ImageUtil { + public static final int DEFAULT_MAX_WIDTH = 1920; + public static final int DEFAULT_MAX_HEIGHT = 1080; + + + private static final int MASK_A = 0xFF000000; + private static final int MASK_R = 0x00FF0000; + private static final int MASK_G = 0x0000FF00; + private static final int MASK_B = 0x000000FF; + + public static int rgbToY(int r, int g, int b) { + return (((66 * r + 129 * g + 25 * b + 128) >> 8) + 16); + } + + public static int rgbToU(int r, int g, int b) { + return (((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128); + } + + public static int rgbToV(int r, int g, int b) { + return (((112 * r - 94 * g - 18 * b + 128) >> 8) + 128); + } + + public static void drawRectOnNv21(byte[] nv21, int width, int height, int color, int strokeWidth, Rect rect) { + if (rect == null || rect.isEmpty()) { + return; + } + drawRectOnNv21(nv21, width, height, color, strokeWidth, rect.left, rect.top, rect.right, rect.bottom); + } + + public static void drawRectOnNv21(byte[] nv21, int width, int height, int color, int strokeWidth, int left, int top, + int right, int bottom) { + if ((strokeWidth & 1) == 1) { + strokeWidth += 1; + } + // 确保边界是4的倍数 + left &= ~0b11; + top &= ~0b11; + right &= ~0b11; + bottom &= ~0b11; + // 对于溢出图像的边,不绘制 + boolean drawLeft = true, drawTop = true, drawRight = true, drawBottom = true; + if (left <= 0) { + left = 0; + drawLeft = false; + } + if (top <= 0) { + top = 0; + drawTop = false; + } + if (right >= width) { + right = width; + drawRight = false; + } + if (bottom >= height) { + bottom = height; + drawBottom = false; + } + + // 取出R G B的值,并转换为Y U V + int r = (color & MASK_R) >> 16; + int g = (color & MASK_G) >> 8; + int b = color & MASK_B; + int y = rgbToY(r, g, b); + int u = rgbToU(r, g, b); + int v = rgbToV(r, g, b); + + // 根据边框的strokeWidth确定内边界 + int innerTop = top + strokeWidth; + int innerBottom = bottom - strokeWidth; + int innerRight = right - strokeWidth; + + int horizontalPixels = right - left; + int yStartIndex; + int uvStartIndex; + boolean drawUV; + if (drawTop) { + yStartIndex = top * width + left; + uvStartIndex = width * height + ((top / 2 * width) + left); + drawUV = false; + for (int i = top; i < innerTop; i++) { + for (int j = 0; j < horizontalPixels; j++) { + nv21[yStartIndex + j] = (byte) y; + } + yStartIndex += width; + if (drawUV = !drawUV) { + for (int j = 0; j < horizontalPixels; j += 2) { + nv21[uvStartIndex + j] = (byte) v; + nv21[uvStartIndex + j + 1] = (byte) u; + } + uvStartIndex += width; + } + } + } + + if (drawLeft) { + //左边 + yStartIndex = innerTop * width + left; + uvStartIndex = width * height + (innerTop / 2 * width + left); + drawUV = false; + for (int i = innerTop; i < innerBottom; i++) { + for (int j = 0; j < strokeWidth; j++) { + nv21[yStartIndex + j] = (byte) y; + } + yStartIndex += width; + if (drawUV = !drawUV) { + for (int j = 0; j < strokeWidth; j += 2) { + nv21[uvStartIndex + j] = (byte) v; + nv21[uvStartIndex + j + 1] = (byte) u; + } + uvStartIndex += width; + } + } + } + if (drawRight) { + //右边 + yStartIndex = innerTop * width + innerRight; + uvStartIndex = width * height + (innerTop / 2 * width + innerRight); + drawUV = false; + for (int i = innerTop; i < innerBottom; i++) { + for (int j = 0; j < strokeWidth; j++) { + nv21[yStartIndex + j] = (byte) y; + } + yStartIndex += width; + if (drawUV = !drawUV) { + for (int j = 0; j < strokeWidth; j += 2) { + nv21[uvStartIndex + j] = (byte) v; + nv21[uvStartIndex + j + 1] = (byte) u; + } + uvStartIndex += width; + } + } + } + + if (drawBottom) { + //下边 + yStartIndex = innerBottom * width + left; + uvStartIndex = width * height + ((innerBottom / 2 * width) + left); + drawUV = false; + for (int i = innerBottom; i < bottom; i++) { + for (int j = 0; j < horizontalPixels; j++) { + nv21[yStartIndex + j] = (byte) y; + } + yStartIndex += width; + if (drawUV = !drawUV) { + for (int j = 0; j < horizontalPixels; j += 2) { + nv21[uvStartIndex + j] = (byte) v; + nv21[uvStartIndex + j + 1] = (byte) u; + } + uvStartIndex += width; + } + } + } + } + + /** + * 缩放图像,如果需要缩放,就顺便把宽高对齐给做了 + * + * @param bitmap 原图 + * @param maxWidth 最大目标宽度 + * @param maxHeight 最大目标高度 + * @return 缩放后的图像 + */ + public static Bitmap scaleBitmap(Bitmap bitmap, int maxWidth, int maxHeight) { + float horizontalScale = ((float) bitmap.getWidth()) / maxWidth; + float verticalScale = ((float) bitmap.getHeight()) / maxHeight; + if (horizontalScale < 1 || verticalScale < 1) { + return bitmap; + } + float maxScale = Math.max(horizontalScale, verticalScale); + // 确保为4的倍数 + int newWidth = (int) (bitmap.getWidth() / maxScale) & ~0b11; + int newHeight = (int) (bitmap.getHeight() / maxScale) & ~0b11; + + return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true); + } + + /** + * 将Uri转换为Bitmap,并限制最大宽高 + */ + public static Bitmap uriToScaledBitmap(Context context, Uri uri, int maxWidth, int maxHeight) { + ContentResolver contentResolver = context.getContentResolver(); + byte[] data; + try { + InputStream input = null; + input = contentResolver.openInputStream(uri); + data = new byte[input.available()]; + input.read(data); + input.close(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + return jpegToScaledBitmap(data, maxWidth, maxHeight); + } + + /** + * 将jpeg形式的压缩图像转换为Bitmap,并限制最大宽高 + * + * @param jpeg jpeg图像数据 + * @param maxWidth 限制的最大宽度 + * @param maxHeight 限制的最大高度 + * @return 宽高小于限制值的Bitmap对象 + */ + public static Bitmap jpegToScaledBitmap(byte[] jpeg, int maxWidth, int maxHeight) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options); + + int inSampleSize = 1; + while (options.outWidth / inSampleSize > maxWidth || options.outHeight / inSampleSize > maxHeight) { + inSampleSize++; + } + options.inSampleSize = inSampleSize; + options.inJustDecodeBounds = false; + return BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options); + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/PreviewConfig.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/PreviewConfig.java new file mode 100644 index 0000000..82098f1 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/PreviewConfig.java @@ -0,0 +1,58 @@ +package com.sw.plate.utils.arcface; + +import android.hardware.Camera; + +public class PreviewConfig { + /** + * 默认的可见光相机ID + */ + public static final int DEFAULT_RGB_CAMERA_ID = Camera.CameraInfo.CAMERA_FACING_BACK; + /** + * 默认的红外相机ID + */ + public static final int DEFAULT_IR_CAMERA_ID = Camera.CameraInfo.CAMERA_FACING_FRONT; + + private int rgbCameraId; + private int irCameraId; + private int rgbAdditionalDisplayOrientation; + private int irAdditionalDisplayOrientation; + + public PreviewConfig(int rgbCameraId, int irCameraId, int rgbAdditionalDisplayOrientation, int irAdditionalDisplayOrientation) { + this.rgbCameraId = rgbCameraId; + this.irCameraId = irCameraId; + this.rgbAdditionalDisplayOrientation = rgbAdditionalDisplayOrientation; + this.irAdditionalDisplayOrientation = irAdditionalDisplayOrientation; + } + + public int getRgbCameraId() { + return rgbCameraId; + } + + public int getIrCameraId() { + return irCameraId; + } + + public int getRgbAdditionalDisplayOrientation() { + return rgbAdditionalDisplayOrientation; + } + + public int getIrAdditionalDisplayOrientation() { + return irAdditionalDisplayOrientation; + } + + public void setRgbCameraId(int rgbCameraId) { + this.rgbCameraId = rgbCameraId; + } + + public void setIrCameraId(int irCameraId) { + this.irCameraId = irCameraId; + } + + public void setRgbAdditionalDisplayOrientation(int rgbAdditionalDisplayOrientation) { + this.rgbAdditionalDisplayOrientation = rgbAdditionalDisplayOrientation; + } + + public void setIrAdditionalDisplayOrientation(int irAdditionalDisplayOrientation) { + this.irAdditionalDisplayOrientation = irAdditionalDisplayOrientation; + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/callback/BatchRegisterCallback.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/callback/BatchRegisterCallback.java new file mode 100644 index 0000000..6290115 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/callback/BatchRegisterCallback.java @@ -0,0 +1,25 @@ +package com.sw.plate.utils.arcface.callback; + +/** + * 批量注册的回调 + */ +public interface BatchRegisterCallback { + /** + * 批量注册过程中的回调 + * + * @param current 当前已处理的数量 + * @param failed 处理失败的数量 + * @param total 处理总数 + */ + void onProcess(int current, int failed, int total); + + /** + * 批量注册结束的回调 + * + * @param current 当前已处理的数量 + * @param failed 处理失败的数量 + * @param total 处理总数 + * @param errMsg 错误消息 + */ + void onFinish(int current, int failed, int total, String errMsg); +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/callback/OnRegisterFinishedCallback.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/callback/OnRegisterFinishedCallback.java new file mode 100644 index 0000000..a849a86 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/callback/OnRegisterFinishedCallback.java @@ -0,0 +1,20 @@ + +package com.sw.plate.utils.arcface.callback; + + +import com.sw.plate.utils.arcface.face.model.FacePreviewInfo; +import com.sw.plate.utils.arcface.model.UserFaceInfo; + +/** + * 实时注册的结果回调 + */ +public interface OnRegisterFinishedCallback { + /** + * 注册结束的回调 + * + * @param facePreviewInfo 注册的人脸信息 + * @param userFaceInfo 是否成功 + */ +// void onRegisterFinished(FacePreviewInfo facePreviewInfo, boolean success); + void onRegisterFinished(FacePreviewInfo facePreviewInfo, UserFaceInfo userFaceInfo); +} \ No newline at end of file diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/CameraHelper.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/CameraHelper.java new file mode 100644 index 0000000..4acbd97 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/CameraHelper.java @@ -0,0 +1,433 @@ +package com.sw.plate.utils.arcface.camera; + +import android.graphics.ImageFormat; +import android.graphics.Point; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.util.Log; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.TextureView; +import android.view.View; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +/** + * 相机辅助类,和{@link CameraListener}共同使用,获取nv21数据等操作 + */ +public class CameraHelper implements Camera.PreviewCallback { + private static final String TAG = "CameraHelper"; + private volatile Camera mCamera; + private int mCameraId; + private Point previewViewSize; + private View previewDisplayView; + private Camera.Size previewSize; + private Point specificPreviewSize; + private int displayOrientation = 0; + private int rotation; + private int additionalRotation; + private boolean isMirror = false; + + private Integer specificCameraId = null; + private CameraListener cameraListener; + + private CameraHelper(Builder builder) { + previewDisplayView = builder.previewDisplayView; + specificCameraId = builder.specificCameraId; + cameraListener = builder.cameraListener; + rotation = builder.rotation; + additionalRotation = builder.additionalRotation; + previewViewSize = builder.previewViewSize; + specificPreviewSize = builder.previewSize; + if (builder.previewDisplayView instanceof TextureView) { + isMirror = builder.isMirror; + } else if (isMirror) { + throw new RuntimeException("mirror is effective only when the preview is on a textureView"); + } + } + + public void init() { + if (previewDisplayView instanceof TextureView) { + ((TextureView) this.previewDisplayView).setSurfaceTextureListener(textureListener); + } else if (previewDisplayView instanceof SurfaceView) { + ((SurfaceView) previewDisplayView).getHolder().addCallback(surfaceCallback); + } + + if (isMirror) { + previewDisplayView.setScaleX(-1); + } + } + + public int getSensorOrientation() { + Camera.CameraInfo info = new Camera.CameraInfo(); + Camera.getCameraInfo(mCameraId, info); + return info.orientation; + } + + public synchronized void start() { + if (mCamera != null) { + return; + } + //相机数量为2则打开1,1则打开0,相机ID 1为前置,0为后置 + mCameraId = Camera.getNumberOfCameras() - 1; + //若指定了相机ID且该相机存在,则打开指定的相机 + if (specificCameraId != null && specificCameraId <= mCameraId) { + mCameraId = specificCameraId; + } + + //没有相机 + if (mCameraId == -1) { + if (cameraListener != null) { + cameraListener.onCameraError(new Exception("camera not found")); + } + return; + } + if (mCamera == null) { + mCamera = Camera.open(mCameraId); + } + + displayOrientation = getCameraOri(rotation); + mCamera.setDisplayOrientation(displayOrientation); + try { + Camera.Parameters parameters = mCamera.getParameters(); + parameters.setPreviewFormat(ImageFormat.NV21); + + // 预览大小设置 + previewSize = parameters.getPreviewSize(); + List supportedPreviewSizes = parameters.getSupportedPreviewSizes(); + if (supportedPreviewSizes != null && supportedPreviewSizes.size() > 0) { + previewSize = getBestSupportedSize(supportedPreviewSizes, previewViewSize); + } + parameters.setPreviewSize(previewSize.width, previewSize.height); + + // 对焦模式设置 + List supportedFocusModes = parameters.getSupportedFocusModes(); + if (supportedFocusModes != null && supportedFocusModes.size() > 0) { + if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { + parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); + } else if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { + parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); + } else if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) { + parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); + } + } + mCamera.setParameters(parameters); + if (previewDisplayView instanceof TextureView) { + mCamera.setPreviewTexture(((TextureView) previewDisplayView).getSurfaceTexture()); + } else { + mCamera.setPreviewDisplay(((SurfaceView) previewDisplayView).getHolder()); + } + mCamera.setPreviewCallback(this); + mCamera.startPreview(); + if (cameraListener != null) { + cameraListener.onCameraOpened(mCamera, mCameraId, displayOrientation, isMirror); + } + } catch (Exception e) { + if (cameraListener != null) { + cameraListener.onCameraError(e); + } + } + } + + private int getCameraOri(int rotation) { + int degrees = rotation * 90; + switch (rotation) { + case Surface.ROTATION_0: + degrees = 0; + break; + case Surface.ROTATION_90: + degrees = 90; + break; + case Surface.ROTATION_180: + degrees = 180; + break; + case Surface.ROTATION_270: + degrees = 270; + break; + default: + break; + } + additionalRotation /= 90; + additionalRotation *= 90; + degrees += additionalRotation; + int result; + Camera.CameraInfo info = new Camera.CameraInfo(); + Camera.getCameraInfo(mCameraId, info); + if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + result = (info.orientation + degrees) % 360; + result = (360 - result) % 360; + } else { + result = (info.orientation - degrees + 360) % 360; + } + return result; + } + + public synchronized void stop() { + if (mCamera == null) { + return; + } + mCamera.setPreviewCallback(null); + mCamera.stopPreview(); + mCamera.release(); + mCamera = null; + if (cameraListener != null) { + cameraListener.onCameraClosed(); + } + } + + public synchronized boolean isStopped() { + return mCamera == null; + } + + public void release() { + synchronized (this) { + stop(); + previewDisplayView = null; + specificCameraId = null; + cameraListener = null; + previewViewSize = null; + specificPreviewSize = null; + previewSize = null; + } + } + + private Camera.Size getBestSupportedSize(List sizes, Point previewViewSize) { + if (sizes == null || sizes.size() == 0) { + return mCamera.getParameters().getPreviewSize(); + } + Camera.Size[] tempSizes = sizes.toArray(new Camera.Size[0]); + Arrays.sort(tempSizes, new Comparator() { + @Override + public int compare(Camera.Size o1, Camera.Size o2) { + if (o1.width > o2.width) { + return -1; + } else if (o1.width == o2.width) { + return o1.height > o2.height ? -1 : 1; + } else { + return 1; + } + } + }); + sizes = Arrays.asList(tempSizes); + + Camera.Size bestSize = sizes.get(0); + float previewViewRatio; + if (previewViewSize != null) { + previewViewRatio = (float) previewViewSize.x / (float) previewViewSize.y; + } else { + previewViewRatio = (float) bestSize.width / (float) bestSize.height; + } + + if (previewViewRatio > 1) { + previewViewRatio = 1 / previewViewRatio; + } + boolean isNormalRotate = (additionalRotation % 180 == 0); + Log.i(TAG, "getBestSupportedSize previewViewSize: " + previewViewSize.toString()); + for (Camera.Size s : sizes) { + if (specificPreviewSize != null && specificPreviewSize.x == s.width && specificPreviewSize.y == s.height) { + return s; + } + if (isNormalRotate) { + if (Math.abs((s.height / (float) s.width) - previewViewRatio) < Math.abs(bestSize.height / (float) bestSize.width - previewViewRatio)) { + bestSize = s; + } + } else { + if (Math.abs((s.width / (float) s.height) - previewViewRatio) < Math.abs(bestSize.width / (float) bestSize.height - previewViewRatio)) { + bestSize = s; + } + } + } + Log.i(TAG, "getBestSupportedSize bestSize: " + bestSize.width + "x" + bestSize.height); + return bestSize; + } + + public List getSupportedPreviewSizes() { + if (mCamera == null) { + return null; + } + return mCamera.getParameters().getSupportedPreviewSizes(); + } + + public List getSupportedPictureSizes() { + if (mCamera == null) { + return null; + } + return mCamera.getParameters().getSupportedPictureSizes(); + } + + + @Override + public void onPreviewFrame(byte[] nv21, Camera camera) { + if (cameraListener != null) { + cameraListener.onPreview(nv21, camera); + } + } + + private TextureView.SurfaceTextureListener textureListener = new TextureView.SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { +// start(); + if (mCamera != null) { + try { + mCamera.setPreviewTexture(surfaceTexture); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) { + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { + stop(); + return false; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { + + } + }; + private SurfaceHolder.Callback surfaceCallback = new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(SurfaceHolder holder) { +// start(); + if (mCamera != null) { + try { + mCamera.setPreviewDisplay(holder); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + stop(); + } + }; + + public void changeDisplayOrientation(int rotation) { + if (mCamera != null) { + this.rotation = rotation; + displayOrientation = getCameraOri(rotation); + mCamera.setDisplayOrientation(displayOrientation); + if (cameraListener != null) { + cameraListener.onCameraConfigurationChanged(mCameraId, displayOrientation); + } + } + } + + public static final class Builder { + + /** + * 预览显示的view,目前仅支持surfaceView和textureView + */ + private View previewDisplayView; + + /** + * 是否镜像显示,只支持textureView + */ + private boolean isMirror; + /** + * 指定的相机ID + */ + private Integer specificCameraId; + /** + * 事件回调 + */ + private CameraListener cameraListener; + /** + * 屏幕的长宽,在选择最佳相机比例时用到 + */ + private Point previewViewSize; + /** + * 传入getWindowManager().getDefaultDisplay().getRotation()的值即可 + */ + private int rotation; + /** + * 指定的预览宽高,若系统支持则会以这个预览宽高进行预览 + */ + private Point previewSize; + + /** + * 额外的旋转角度(用于适配一些定制设备) + */ + private int additionalRotation; + + public Builder() { + } + + + public Builder previewOn(View val) { + if (val instanceof SurfaceView || val instanceof TextureView) { + previewDisplayView = val; + return this; + } else { + throw new RuntimeException("you must preview on a textureView or a surfaceView"); + } + } + + + public Builder isMirror(boolean val) { + isMirror = val; + return this; + } + + public Builder previewSize(Point val) { + previewSize = val; + return this; + } + + public Builder previewViewSize(Point val) { + previewViewSize = val; + return this; + } + + public Builder rotation(int val) { + rotation = val; + return this; + } + + public Builder additionalRotation(int val) { + additionalRotation = val; + return this; + } + + public Builder specificCameraId(Integer val) { + specificCameraId = val; + return this; + } + + public Builder cameraListener(CameraListener val) { + cameraListener = val; + return this; + } + + public CameraHelper build() { + if (previewViewSize == null) { + Log.e(TAG, "previewViewSize is null, now use default previewSize"); + } + if (cameraListener == null) { + Log.e(TAG, "cameraListener is null, callback will not be called"); + } + if (previewDisplayView == null) { + throw new RuntimeException("you must preview on a textureView or a surfaceView"); + } + return new CameraHelper(this); + } + } + +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/CameraListener.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/CameraListener.java new file mode 100644 index 0000000..2880feb --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/CameraListener.java @@ -0,0 +1,44 @@ +package com.sw.plate.utils.arcface.camera; + +import android.hardware.Camera; + + +public interface CameraListener { + /** + * 当打开时执行 + * + * @param camera 相机实例 + * @param cameraId 相机ID + * @param displayOrientation 相机预览旋转角度 + * @param isMirror 是否镜像显示 + */ + void onCameraOpened(Camera camera, int cameraId, int displayOrientation, boolean isMirror); + + /** + * 预览数据回调 + * + * @param data 预览数据 + * @param camera 相机实例 + */ + void onPreview(byte[] data, Camera camera); + + /** + * 当相机关闭时执行 + */ + void onCameraClosed(); + + /** + * 当出现异常时执行 + * + * @param e 相机相关异常 + */ + void onCameraError(Exception e); + + /** + * 属性变化时调用 + * + * @param cameraID 相机ID + * @param displayOrientation 相机旋转方向 + */ + void onCameraConfigurationChanged(int cameraID, int displayOrientation); +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/DualCameraHelper.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/DualCameraHelper.java new file mode 100644 index 0000000..f8b5825 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/DualCameraHelper.java @@ -0,0 +1,580 @@ +package com.sw.plate.utils.arcface.camera; + +import android.graphics.Bitmap; +import android.graphics.ImageFormat; +import android.graphics.Point; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.util.Log; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.TextureView; +import android.view.View; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +/** + * 打开两个相机的辅助类 + *

+ * 由于IR摄像头和RGB摄像头的默认分辨率可能不同,为了让两者相同,该类做了以下操作: + * 1. 获取两者支持的分辨率列表到到静态变量{@link DualCameraHelper#rgbSupportedPreviewSizes}及{@link DualCameraHelper#irSupportedPreviewSizes}中, + * 2. 使用{@link DualCameraHelper#getCommonSupportedPreviewSize()}方法取分辨率的交集, + * 3. 使用{@link DualCameraHelper#getBestSupportedSize(List, Point)}取最佳分辨率使两个摄像头分辨率尽可能相同 + */ +public class DualCameraHelper implements Camera.PreviewCallback { + private static List rgbSupportedPreviewSizes; + private static List irSupportedPreviewSizes; + private static final String TAG = "CameraHelper"; + private Camera mCamera; + private int mCameraId; + private Point previewViewSize; + private View previewDisplayView; + private Camera.Size previewSize; + private Point specificPreviewSize; + private int displayOrientation = 0; + private int rotation; + private int additionalRotation; + private boolean isMirror = false; + + private Integer specificCameraId = null; + private CameraListener cameraListener; +// private static final int MIN_PREVIEW_WIDTH = 720; +// private static final int MIN_PREVIEW_HEIGHT = 720; + private static final int MIN_PREVIEW_WIDTH = 800; + private static final int MIN_PREVIEW_HEIGHT = 600; + + private DualCameraHelper(Builder builder) { + previewDisplayView = builder.previewDisplayView; + specificCameraId = builder.specificCameraId; + cameraListener = builder.cameraListener; + rotation = builder.rotation; + additionalRotation = builder.additionalRotation; + previewViewSize = builder.previewViewSize; + specificPreviewSize = builder.previewSize; + if (builder.previewDisplayView instanceof TextureView) { + isMirror = builder.isMirror; + } else if (isMirror) { + throw new RuntimeException("mirror is effective only when the preview is on a textureView"); + } + } + + public interface SurfaceFrameCallback { + void onFrameCallback(Bitmap frame); + } + + private SurfaceFrameCallback surfaceFrameCallback; + + public void setSurfaceFrameCallback(SurfaceFrameCallback callback) { + this.surfaceFrameCallback = callback; + } + + public void init() { + if (previewDisplayView instanceof TextureView) { + ((TextureView) this.previewDisplayView).setSurfaceTextureListener(textureListener); + } else if (previewDisplayView instanceof SurfaceView) { + ((SurfaceView) previewDisplayView).getHolder().addCallback(surfaceCallback); + } + + if (isMirror) { + previewDisplayView.setScaleX(-1); + } + } + + public List getCommonSupportedPreviewSize() { + /** + * irSupportedPreviewSizes 和 rgbSupportedPreviewSizes 为null才去获取, + * 不为null就没必要获取了,而且此时有可能该camera已处于打开状态,无法打开camera + */ + if (rgbSupportedPreviewSizes == null) { + Camera rgbCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK); +// Camera rgbCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT); + rgbSupportedPreviewSizes = rgbCamera.getParameters().getSupportedPreviewSizes(); + rgbCamera.release(); + } + try { + if (irSupportedPreviewSizes == null) { + Camera irCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT); +// Camera irCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK); + irSupportedPreviewSizes = irCamera.getParameters().getSupportedPreviewSizes(); + irCamera.release(); + } + } catch (RuntimeException e) { + e.printStackTrace(); + irSupportedPreviewSizes = rgbSupportedPreviewSizes; + } + List commonPreviewSizes = new ArrayList<>(); + for (Camera.Size rgbPreviewSize : rgbSupportedPreviewSizes) { + if (rgbPreviewSize.width < MIN_PREVIEW_WIDTH || rgbPreviewSize.height < MIN_PREVIEW_HEIGHT) { + continue; + } + for (Camera.Size irPreviewSize : irSupportedPreviewSizes) { + if (irPreviewSize.width == rgbPreviewSize.width && irPreviewSize.height == rgbPreviewSize.height) { + commonPreviewSizes.add(rgbPreviewSize); + } + } + } + return commonPreviewSizes; + } + + /** + * 回传当前使用的cameraID,若当前没打开相机,回传-1 + * + * @return cameraId,失败回传-1 + */ + public int getCurrentOpenedCameraId() { + if (mCamera == null) { + return -1; + } + return mCameraId; + } + + public void start() { + synchronized (this) { + if (mCamera != null) { + return; + } + List supportedPreviewSize = getCommonSupportedPreviewSize(); + StringBuilder stringBuilder = new StringBuilder(); + for (Camera.Size size : supportedPreviewSize) { + stringBuilder.append("width=").append(size.width).append(",") + .append("height=").append(size.height).append(","); + } + Log.d(TAG, "start: "+stringBuilder); + //相机数量为2则打开1,1则打开0,相机ID 1为前置,0为后置 + mCameraId = Camera.getNumberOfCameras() - 1; + //若指定了相机ID且该相机存在,则打开指定的相机 +// if (specificCameraId != null && specificCameraId <= mCameraId) { + if (specificCameraId != null) { + mCameraId = specificCameraId; + } + + //没有相机 + if (mCameraId == -1) { + if (cameraListener != null) { + cameraListener.onCameraError(new Exception("camera not found")); + } + return; + } + if (mCamera == null) { + mCamera = Camera.open(mCameraId); + } + displayOrientation = getCameraOri(rotation); + mCamera.setDisplayOrientation(displayOrientation); + try { + Camera.Parameters parameters = mCamera.getParameters(); + parameters.setPreviewFormat(ImageFormat.NV21); + + //预览大小设置 + previewSize = parameters.getPreviewSize(); + if (supportedPreviewSize != null && supportedPreviewSize.size() > 0) { + previewSize = getBestSupportedSize(supportedPreviewSize, previewViewSize); + } + Log.i(TAG, "start: " + previewSize.width + "x" + previewSize.height); + parameters.setPreviewSize(previewSize.width, previewSize.height); + + //对焦模式设置 + List supportedFocusModes = parameters.getSupportedFocusModes(); + if (supportedFocusModes != null && supportedFocusModes.size() > 0) { + if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { + parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); + } else if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { + parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); + } else if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) { + parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); + } + } + mCamera.setParameters(parameters); + if (previewDisplayView instanceof TextureView) { + mCamera.setPreviewTexture(((TextureView) previewDisplayView).getSurfaceTexture()); + } else { + mCamera.setPreviewDisplay(((SurfaceView) previewDisplayView).getHolder()); + } + mCamera.setPreviewCallback(this); + mCamera.startPreview(); + if (cameraListener != null) { + cameraListener.onCameraOpened(mCamera, mCameraId, displayOrientation, isMirror); + } + } catch (Exception e) { + if (cameraListener != null) { + cameraListener.onCameraError(e); + } + } + } + } + + public void switchCameraId() { + mCameraId = 1 - mCameraId; + if (specificCameraId != null) { + specificCameraId = 1 - specificCameraId; + } + } + + private int getCameraOri(int rotation) { + int degrees = rotation * 90; + switch (rotation) { + case Surface.ROTATION_0: + degrees = 0; + break; + case Surface.ROTATION_90: + degrees = 90; + break; + case Surface.ROTATION_180: + degrees = 180; + break; + case Surface.ROTATION_270: + degrees = 270; + break; + default: + break; + } + additionalRotation /= 90; + additionalRotation *= 90; + degrees += additionalRotation; + int result; + Camera.CameraInfo info = new Camera.CameraInfo(); + Camera.getCameraInfo(mCameraId, info); + if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + result = (info.orientation + degrees) % 360; + result = (360 - result) % 360; + } else { + result = (info.orientation - degrees + 360) % 360; + } + return result; + } + + /** + * 停止预览 + */ + public void stop() { + synchronized (this) { + if (mCamera == null) { + return; + } + mCamera.setPreviewCallback(null); + mCamera.stopPreview(); + mCamera.release(); + mCamera = null; + if (cameraListener != null) { + cameraListener.onCameraClosed(); + } + } + } + + public boolean isStopped() { + synchronized (this) { + return mCamera == null; + } + } + + /** + * 释放操作 + */ + public void release() { + synchronized (this) { + stop(); + previewDisplayView = null; + specificCameraId = null; + cameraListener = null; + previewViewSize = null; + specificPreviewSize = null; + previewSize = null; + } + } + + /** + * 获取候选分辨率列表中最接近预览view大小的分辨率 + * + * @param sizes 支持的分辨率 + * @param previewViewSize 预览view的大小 + * @return 最接近预览view大小的分辨率 + */ + private Camera.Size getBestSupportedSize(List sizes, Point previewViewSize) { + if (sizes == null || sizes.size() == 0) { + return mCamera.getParameters().getPreviewSize(); + } + Camera.Size[] tempSizes = sizes.toArray(new Camera.Size[0]); + Arrays.sort(tempSizes, new Comparator() { + @Override + public int compare(Camera.Size o1, Camera.Size o2) { + if (o1.width > o2.width) { + return -1; + } else if (o1.width == o2.width) { + return o1.height > o2.height ? -1 : 1; + } else { + return 1; + } + } + }); + sizes = Arrays.asList(tempSizes); + + Camera.Size bestSize = sizes.get(0); + float previewViewRatio; + if (previewViewSize != null) { + previewViewRatio = (float) previewViewSize.x / (float) previewViewSize.y; + } else { + previewViewRatio = (float) bestSize.width / (float) bestSize.height; + } + + if (previewViewRatio > 1) { + previewViewRatio = 1 / previewViewRatio; + } + boolean isNormalRotate = (additionalRotation % 180 == 0); + + for (Camera.Size s : sizes) { + if (specificPreviewSize != null && specificPreviewSize.x == s.width && specificPreviewSize.y == s.height) { + return s; + } + if (isNormalRotate) { + if (Math.abs((s.height / (float) s.width) - previewViewRatio) < Math.abs(bestSize.height / (float) bestSize.width - previewViewRatio)) { + bestSize = s; + } + } else { + if (Math.abs((s.width / (float) s.height) - previewViewRatio) < Math.abs(bestSize.width / (float) bestSize.height - previewViewRatio)) { + bestSize = s; + } + } + } + return bestSize; + } + + public List getSupportedPreviewSizes() { + if (mCamera == null) { + return null; + } + return mCamera.getParameters().getSupportedPreviewSizes(); + } + + public List getSupportedPictureSizes() { + if (mCamera == null) { + return null; + } + return mCamera.getParameters().getSupportedPictureSizes(); + } + + + @Override + public void onPreviewFrame(byte[] nv21, Camera camera) { + if (cameraListener != null) { + cameraListener.onPreview(nv21, camera); + } + } + + private TextureView.SurfaceTextureListener textureListener = new TextureView.SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { +// start(); + if (mCamera != null) { + try { + mCamera.setPreviewTexture(surfaceTexture); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) { + Log.i(TAG, "onSurfaceTextureSizeChanged: " + width + " " + height); + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { + stop(); + return false; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { + if (surfaceFrameCallback!=null) { + if (previewDisplayView instanceof TextureView) { + Bitmap frame = ((TextureView) previewDisplayView).getBitmap(); + surfaceFrameCallback.onFrameCallback(frame); + } + } + } + }; + private SurfaceHolder.Callback surfaceCallback = new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(SurfaceHolder holder) { +// start(); + if (mCamera != null) { + try { + mCamera.setPreviewDisplay(holder); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + stop(); + } + }; + + public void changeDisplayOrientation(int rotation) { + if (mCamera != null) { + this.rotation = rotation; + displayOrientation = getCameraOri(rotation); + mCamera.setDisplayOrientation(displayOrientation); + if (cameraListener != null) { + cameraListener.onCameraConfigurationChanged(mCameraId, displayOrientation); + } + } + } + + public static final class Builder { + + /** + * 预览显示的view,目前仅支持surfaceView和textureView + */ + private View previewDisplayView; + + /** + * 是否镜像显示,只支持textureView + */ + private boolean isMirror; + /** + * 指定的相机ID + */ + private Integer specificCameraId; + /** + * 事件回调 + */ + private CameraListener cameraListener; + /** + * 屏幕的长宽,在选择最佳相机比例时用到 + */ + private Point previewViewSize; + /** + * 传入getWindowManager().getDefaultDisplay().getRotation()的值即可 + */ + private int rotation; + /** + * 指定的预览宽高,若系统支持则会以这个预览宽高进行预览 + */ + private Point previewSize; + + /** + * 额外的旋转角度(用于适配一些定制设备) + */ + private int additionalRotation; + + public Builder() { + } + + + public Builder previewOn(View val) { + if (val instanceof SurfaceView || val instanceof TextureView) { + previewDisplayView = val; + return this; + } else { + throw new RuntimeException("you must preview on a textureView or a surfaceView"); + } + } + + + public Builder isMirror(boolean val) { + isMirror = val; + return this; + } + + public Builder previewSize(Point val) { + previewSize = val; + return this; + } + + public Builder previewViewSize(Point val) { + previewViewSize = val; + return this; + } + + public Builder rotation(int val) { + rotation = val; + return this; + } + + public Builder additionalRotation(int val) { + additionalRotation = val; + return this; + } + + public Builder specificCameraId(Integer val) { + specificCameraId = val; + return this; + } + + public Builder cameraListener(CameraListener val) { + cameraListener = val; + return this; + } + + public DualCameraHelper build() { + if (previewViewSize == null) { + Log.e(TAG, "previewViewSize is null, now use default previewSize"); + } + if (cameraListener == null) { + Log.e(TAG, "cameraListener is null, callback will not be called"); + } + if (previewDisplayView == null) { + throw new RuntimeException("you must preview on a textureView or a surfaceView"); + } + return new DualCameraHelper(this); + } + } + + /** + * 根据设置的额外旋转角度旋转 + * + * @param additionalRotation 额外旋转角度 + * @return 当前显示旋转角度 + */ + public int rotateAdditional(int additionalRotation) { + this.additionalRotation = additionalRotation; + int cameraOri = getCameraOri(rotation); + if (mCamera == null) { + start(); + return cameraOri; + } + mCamera.setDisplayOrientation(cameraOri); + return cameraOri; + } + + public void setSpecificPreviewSize(Point specificPreviewSize) { + this.specificPreviewSize = specificPreviewSize; + } + + public static boolean hasDualCamera() { + return Camera.getNumberOfCameras() > 1; + } + + public static boolean canOpenDualCamera() { + Camera camera0 = null; + Camera camera1 = null; + boolean can = true; + try { + camera0 = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK); + camera1 = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT); + } catch (Exception e) { + can = false; + } + if (camera0 != null) { + camera0.release(); + } + if (camera1 != null) { + camera1.release(); + } + return can; + } + +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/glsurface/CameraGLSurfaceView.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/glsurface/CameraGLSurfaceView.java new file mode 100644 index 0000000..245f66d --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/glsurface/CameraGLSurfaceView.java @@ -0,0 +1,98 @@ +package com.sw.plate.utils.arcface.camera.glsurface; + +import android.content.Context; +import android.graphics.Rect; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.util.AttributeSet; +import android.util.Log; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +public class CameraGLSurfaceView extends GLSurfaceView { + private static final String TAG = "CameraGLSurfaceView"; + + + YUVRenderer yuvRenderer; + NV21Drawer nv21Drawer; + + public CameraGLSurfaceView(Context context) { + this(context, null); + } + + public CameraGLSurfaceView(Context context, AttributeSet attrs) { + super(context, attrs); + setEGLContextClientVersion(2); + // 设置Renderer到GLSurfaceView + yuvRenderer = new YUVRenderer(); + nv21Drawer = new NV21Drawer(); + setRenderer(yuvRenderer); + // 只有在绘制数据改变时才绘制view + setRenderMode(RENDERMODE_WHEN_DIRTY); + } + + /** + * 设置不同的片段着色器代码以达到不同的预览效果 + * + * @param fragmentShaderCode 片段着色器代码 + */ + public void setFragmentShaderCode(String fragmentShaderCode) { + nv21Drawer.setFragmentShaderCode(fragmentShaderCode); + } + + public void init(boolean isMirror, int rotateDegree, int frameWidth, int frameHeight) { + nv21Drawer.init(isMirror, rotateDegree, frameWidth, frameHeight); + + queueEvent(() -> yuvRenderer.initRenderer()); + } + + public class YUVRenderer implements Renderer { + private void initRenderer() { + boolean createSuccess = nv21Drawer.createGLProgram(); + if (!createSuccess) { + Log.e(TAG, "initRenderer createGLProgram failed!"); + } + } + + @Override + public void onSurfaceCreated(GL10 unused, EGLConfig config) { + Log.i(TAG, "initRenderer onSurfaceCreated: "); + initRenderer(); + } + + + @Override + public void onDrawFrame(GL10 gl) { + nv21Drawer.render(); + } + + @Override + public void onSurfaceChanged(GL10 unused, int width, int height) { + Log.i(TAG, "onSurfaceChanged: "); + GLES20.glViewport(0, 0, width, height); + } + } + + /** + * 传入NV21刷新帧 + * + * @param data NV21数据 + */ + public void renderNV21(byte[] data) { + nv21Drawer.updateNV21(data); + requestRender(); + } + + + /** + * 传入NV21刷新帧,并同时绘制人脸框 + * + * @param data NV21数据 + * @param faceRect 人脸框 + */ + public void renderNV21WithFaceRect(byte[] data, Rect faceRect, int strokeWidth) { + nv21Drawer.updateNV21(data, faceRect, strokeWidth); + requestRender(); + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/glsurface/GLUtil.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/glsurface/GLUtil.java new file mode 100644 index 0000000..387e2a4 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/glsurface/GLUtil.java @@ -0,0 +1,266 @@ +package com.sw.plate.utils.arcface.camera.glsurface; + +import android.opengl.GLES20; +import android.util.Log; + +import java.nio.IntBuffer; + +public class GLUtil { + private static final String TAG = "GLUtil"; + + + /** + * 显示的顶点 + */ + static final float[] SQUARE_VERTICES = { + -1.0f, -1.0f, + 1.0f, -1.0f, + -1.0f, 1.0f, + 1.0f, 1.0f + }; + /** + * 原数据显示 + * 0,1***********1,1 + * * * + * * * + * * * + * * * + * * * + * 0,0***********1,0 + */ + static final float[] COORD_VERTICES = { + 0.0f, 1.0f, + 1.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 0.0f + }; + + /** + * 逆时针旋转90度显示 + * 1,1***********1,0 + * * * + * * * + * * * + * * * + * * * + * 0,1***********0,0 + */ + static final float[] ROTATE_90_COORD_VERTICES = { + 1.0f, 1.0f, + 1.0f, 0.0f, + 0.0f, 1.0f, + 0.0f, 0.0f + }; + + /** + * 逆时针旋转180度显示 + * 1,0***********0,0 + * * * + * * * + * * * + * * * + * * * + * 1,1***********0,1 + */ + static final float[] ROTATE_180_COORD_VERTICES = { + 1.0f, 0.0f, + 0.0f, 0.0f, + 1.0f, 1.0f, + 0.0f, 1.0f + }; + + /** + * 逆时针旋转270度显示 + * 0,0***********0,1 + * * * + * * * + * * * + * * * + * * * + * 1,0***********1,1 + */ + static final float[] ROTATE_270_COORD_VERTICES = { + 0.0f, 0.0f, + 0.0f, 1.0f, + 1.0f, 0.0f, + 1.0f, 1.0f + }; + + /** + * 镜像显示 + * 1,1***********0,1 + * * * + * * * + * * * + * * * + * * * + * 1,0***********0,0 + */ + static final float[] MIRROR_COORD_VERTICES = { + 1.0f, 1.0f, + 0.0f, 1.0f, + 1.0f, 0.0f, + 0.0f, 0.0f + }; + + /** + * 镜像并逆时针旋转90度显示 + * 0,1***********0,0 + * * * + * * * + * * * + * * * + * * * + * 1,1***********1,0 + */ + static final float[] ROTATE_90_MIRROR_COORD_VERTICES = { + 0.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 1.0f, + 1.0f, 0.0f + }; + /** + * 镜像并逆时针旋转180度显示 + * 0,0***********1,0 + * * * + * * * + * * * + * * * + * * * + * 0,1***********1,1 + */ + static final float[] ROTATE_180_MIRROR_COORD_VERTICES = { + 0.0f, 0.0f, + 1.0f, 0.0f, + 0.0f, 1.0f, + 1.0f, 1.0f + }; + /** + * 镜像并逆时针旋转270度显示 + * 1,0***********1,1 + * * * + * * * + * * * + * * * + * * * + * 0,0***********0,1 + */ + static final float[] ROTATE_270_MIRROR_COORD_VERTICES = { + 1.0f, 0.0f, + 1.0f, 1.0f, + 0.0f, 0.0f, + 0.0f, 1.0f + }; + + /** + * 创建OpenGL Program,并链接 + * + * @param fragmentShaderCode 片段着色器代码 + * @param vertexShaderCode 顶点着色器代码 + * @return OpenGL Program + */ + static int createShaderProgram(String fragmentShaderCode, String vertexShaderCode) { + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode); + int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode); + if (vertexShader == 0 || fragmentShader == 0) { + return 0; + } + int mProgram = GLES20.glCreateProgram(); + GLES20.glAttachShader(mProgram, vertexShader); + GLES20.glAttachShader(mProgram, fragmentShader); + GLES20.glLinkProgram(mProgram); + + IntBuffer linked = IntBuffer.allocate(1); + GLES20.glGetProgramiv(mProgram, GLES20.GL_LINK_STATUS, linked); + if (linked.get(0) == 0) { + return 0; + } + return mProgram; + } + + /** + * 加载着色器 + * + * @param shaderType 着色器类型,可以是片段着色器{@link GLES20#GL_FRAGMENT_SHADER}或顶点着色器{@link GLES20#GL_VERTEX_SHADER} + * @param source 着色器代码 + * @return 着色器对象的引用,0代表失败 + */ + static int loadShader(int shaderType, String source) { + int shader = GLES20.glCreateShader(shaderType); + if (shader == 0) { + Log.e(TAG, "loadShader: failed to create shader"); + checkGlErrorIfOccur("create shader " + shaderType); + return 0; + } + GLES20.glShaderSource(shader, source); + GLES20.glCompileShader(shader); + int[] compiled = new int[1]; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); + if (compiled[0] == 0) { + Log.e(TAG, "Could not compile shader " + shaderType + ":" + GLES20.glGetShaderInfoLog(shader)); + GLES20.glDeleteShader(shader); + shader = 0; + checkGlErrorIfOccur("glGetShaderiv " + shaderType); + } + return shader; + } + + /** + * 检查是否出现GLES错误 + */ + private static void checkGlErrorIfOccur(String op) { + int error = GLES20.glGetError(); + if (error != GLES20.GL_NO_ERROR) { + String errorMsg = String.format("error 0x%h occurred: %s", error, op); + Log.e(TAG, errorMsg); + throw new RuntimeException(errorMsg); + } + } + + /** + * 根据是否镜像和旋转角度选择合适的顶点坐标 + * + * @param isMirror 是否镜像 + * @param rotateDegree 旋转角度 + * @return 顶点坐标 + */ + static float[] getCoordVerticesByPreviewParams(boolean isMirror, int rotateDegree) { + float[] coordVertice = GLUtil.COORD_VERTICES; + if (isMirror) { + switch (rotateDegree) { + case 0: + coordVertice = GLUtil.MIRROR_COORD_VERTICES; + break; + case 90: + coordVertice = GLUtil.ROTATE_90_MIRROR_COORD_VERTICES; + break; + case 180: + coordVertice = GLUtil.ROTATE_180_MIRROR_COORD_VERTICES; + break; + case 270: + coordVertice = GLUtil.ROTATE_270_MIRROR_COORD_VERTICES; + break; + default: + break; + } + } else { + switch (rotateDegree) { + case 0: + coordVertice = GLUtil.COORD_VERTICES; + break; + case 90: + coordVertice = GLUtil.ROTATE_90_COORD_VERTICES; + break; + case 180: + coordVertice = GLUtil.ROTATE_180_COORD_VERTICES; + break; + case 270: + coordVertice = GLUtil.ROTATE_270_COORD_VERTICES; + break; + default: + break; + } + } + return coordVertice.clone(); + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/glsurface/NV21Drawer.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/glsurface/NV21Drawer.java new file mode 100644 index 0000000..cb498fd --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/camera/glsurface/NV21Drawer.java @@ -0,0 +1,297 @@ +package com.sw.plate.utils.arcface.camera.glsurface; + +import android.graphics.Color; +import android.graphics.Rect; +import android.opengl.GLES20; +import android.util.Log; + +import com.sw.plate.utils.arcface.ImageUtil; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.Arrays; + +/** + * 用于绘制NV21数据的封装类 + */ +public class NV21Drawer { + private static final String TAG = "NV21Drawer"; + + + // SQUARE_VERTICES每2个值作为一个顶点 + private static final int COUNT_PER_SQUARE_VERTICE = 2; + // COORD_VERTICES每2个值作为一个顶点 + private static final int COUNT_PER_COORD_VERTICES = 2; + // 一个FLOAT占4个字节,用于分配内存时的计算 + private static final int FLOAT_SIZE_BYTES = 4; + + /** + * 片段着色器,正常效果 + */ + public static final String FRAG_SHADER_NORMAL = + "precision mediump float;\n" + + " varying vec2 tc;\n" + + " uniform sampler2D ySampler;\n" + + " uniform sampler2D vuSampler;\n" + + " const mat3 yuvToRgbMat = mat3(1.0, 1.0, 1.0, 0, -0.344, 1.77, 1.403, -0.714,0);\n" + + " void main()\n" + + " {\n" + + " vec3 yuv;\n" + + " yuv.x = texture2D(ySampler, tc).r;\n" + + " vec4 vuVec = texture2D(vuSampler, tc);\n" + + " yuv.y = vuVec.a - 0.5;\n" + + " yuv.z = vuVec.r - 0.5;\n" + + " gl_FragColor = vec4(yuvToRgbMat * yuv, 1.0);\n" + + " }"; + /** + * 片段着色器,灰度效果。R = G = B = Y + */ + public static final String FRAG_SHADER_GRAY = + "precision mediump float;\n" + + " varying vec2 tc;\n" + + " uniform sampler2D ySampler;\n" + + " void main()\n" + + " {\n" + + " vec3 yuv;\n" + + " yuv.xyz = texture2D(ySampler, tc).rrr;\n" + + " gl_FragColor = vec4(yuv, 1.0);\n" + + " }"; + + /** + * 顶点着色器 + */ + private static final String VERTEX_SHADER = + " attribute vec4 attr_position;\n" + + " attribute vec2 attr_tc;\n" + + " varying vec2 tc;\n" + + " void main() {\n" + + " gl_Position = attr_position;\n" + + " tc = attr_tc;\n" + + " }"; + + // 源视频帧宽/高 + private int frameWidth, frameHeight; + // 是否镜像 + private boolean isMirror; + // 是否旋转 + private int rotateDegree = 0; + + // 用于画框并显示的NV21 + private byte[] nv21WithRect; + + private ByteBuffer yBuf = null, vuBuf = null; + + // 纹理id + private int[] yTexture = new int[1]; + private int[] vuTexture = new int[1]; + + private String fragmentShaderCode = FRAG_SHADER_NORMAL; + + private FloatBuffer squareVertices = null; + private FloatBuffer coordVertices = null; + + private int programHandle = 0; + + // gl_attr + private int glPosition; + private int textureCoord; + + /** + * 设置不同的片段着色器代码以达到不同的预览效果 + * + * @param fragmentShaderCode 片段着色器代码 + */ + public void setFragmentShaderCode(String fragmentShaderCode) { + this.fragmentShaderCode = fragmentShaderCode; + } + + + public void init(boolean isMirror, int rotateDegree, int frameWidth, int frameHeight) { + if (this.frameWidth == frameWidth + && this.frameHeight == frameHeight + && this.rotateDegree == rotateDegree + && this.isMirror == isMirror) { + return; + } + this.frameWidth = frameWidth; + this.frameHeight = frameHeight; + this.rotateDegree = rotateDegree; + this.isMirror = isMirror; + + int yFrameSize = this.frameHeight * this.frameWidth; + int vuFrameSize = yFrameSize / 2; + yBuf = ByteBuffer.allocateDirect(yFrameSize); + vuBuf = ByteBuffer.allocateDirect(vuFrameSize); + + // TODO:这段代码可删除 + // 这里的作用是为VU数据预先填上0x80,避免打开时的瞬间全是绿色 + byte[] vu = new byte[vuFrameSize]; + Arrays.fill(vu, (byte) 0x80); + vuBuf.put(vu); + vuBuf.position(0); + + // 顶点坐标 + squareVertices = ByteBuffer + .allocateDirect(GLUtil.SQUARE_VERTICES.length * FLOAT_SIZE_BYTES) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + squareVertices.put(GLUtil.SQUARE_VERTICES).position(0); + + // 纹理坐标 + float[] coordVertice = GLUtil.getCoordVerticesByPreviewParams(isMirror, rotateDegree); + // 显示多块数据 +// for (int i = 0; i < coordVertice.length; i++) { +// coordVertice[i] *= 2; +// } + coordVertices = ByteBuffer.allocateDirect(coordVertice.length * FLOAT_SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer(); + coordVertices.put(coordVertice).position(0); + + } + + private void createTexture(int width, int height, int format, int[] textureId) { + + GLES20.glGenTextures(1, textureId, 0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId[0]); + + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT); + + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST); + + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, format, width, height, 0, format, GLES20.GL_UNSIGNED_BYTE, null); + } + + /** + * 创建OpenGL Program并关联shader代码中的变量 + */ + public boolean createGLProgram() { + if (squareVertices == null || coordVertices == null) { + return false; + } + programHandle = GLUtil.createShaderProgram(fragmentShaderCode, VERTEX_SHADER); + if (programHandle != 0) { + + GLES20.glUseProgram(programHandle); + + glPosition = GLES20.glGetAttribLocation(programHandle, "attr_position"); + textureCoord = GLES20.glGetAttribLocation(programHandle, "attr_tc"); + + GLES20.glEnableVertexAttribArray(glPosition); + GLES20.glEnableVertexAttribArray(textureCoord); + + squareVertices.position(0); + GLES20.glVertexAttribPointer(glPosition, COUNT_PER_SQUARE_VERTICE, GLES20.GL_FLOAT, false, 8, squareVertices); + coordVertices.position(0); + GLES20.glVertexAttribPointer(textureCoord, COUNT_PER_COORD_VERTICES, GLES20.GL_FLOAT, false, 8, coordVertices); + + + int ySampler = GLES20.glGetUniformLocation(programHandle, "ySampler"); + int vuSampler = GLES20.glGetUniformLocation(programHandle, "vuSampler"); + + GLES20.glUniform1i(ySampler, 0); + GLES20.glUniform1i(vuSampler, 1); + + + //启用纹理 + GLES20.glEnable(GLES20.GL_TEXTURE_2D); + //创建纹理 + createTexture(frameWidth, frameHeight, GLES20.GL_LUMINANCE, yTexture); + createTexture(frameWidth / 2, frameHeight / 2, GLES20.GL_LUMINANCE_ALPHA, vuTexture); + + return true; + } else { + return false; + } + } + + boolean prepareDraw() { + if (programHandle != 0) { + GLES20.glUseProgram(programHandle); + + GLES20.glEnableVertexAttribArray(glPosition); + GLES20.glEnableVertexAttribArray(textureCoord); + + squareVertices.position(0); + GLES20.glVertexAttribPointer(glPosition, COUNT_PER_SQUARE_VERTICE, GLES20.GL_FLOAT, false, 8, squareVertices); + coordVertices.position(0); + GLES20.glVertexAttribPointer(textureCoord, COUNT_PER_COORD_VERTICES, GLES20.GL_FLOAT, false, 8, coordVertices); + + return true; + } else { + Log.e(TAG, "program not created!"); + return false; + } + } + + synchronized boolean render() { + if (vuBuf != null && programHandle != 0) { + + // y + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yTexture[0]); + GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, + 0, + 0, + 0, + frameWidth, + frameHeight, + GLES20.GL_LUMINANCE, + GLES20.GL_UNSIGNED_BYTE, + yBuf); + + // vu + GLES20.glActiveTexture(GLES20.GL_TEXTURE1); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, vuTexture[0]); + GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, + 0, + 0, + 0, + frameWidth / 2, + frameHeight / 2, + GLES20.GL_LUMINANCE_ALPHA, + GLES20.GL_UNSIGNED_BYTE, + vuBuf); + + // 在数据绑定完成后进行绘制 + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + return true; + } + return false; + } + + boolean updateNV21(byte[] data) { + if (vuBuf == null) { + return false; + } + int ySize = frameWidth * frameHeight; + int vuSize = ySize / 2; + synchronized (this) { + yBuf.put(data, 0, ySize).position(0); + vuBuf.put(data, ySize, vuSize).position(0); + } + return true; + } + + boolean updateNV21(byte[] data, Rect faceRect, int strokeWidth) { + if (vuBuf == null) { + return false; + } + // 避免重复创建,频繁GC + if (nv21WithRect == null || nv21WithRect.length != data.length) { + nv21WithRect = new byte[data.length]; + } + System.arraycopy(data, 0, nv21WithRect, 0, nv21WithRect.length); + + ImageUtil.drawRectOnNv21(nv21WithRect, frameWidth, frameHeight, Color.YELLOW, strokeWidth, faceRect); + int ySize = frameWidth * frameHeight; + int vuSize = ySize / 2; + + synchronized (this) { + yBuf.put(nv21WithRect, 0, ySize).position(0); + vuBuf.put(nv21WithRect, ySize, vuSize).position(0); + } + return true; + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/FaceHelper.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/FaceHelper.java new file mode 100644 index 0000000..b3c8675 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/FaceHelper.java @@ -0,0 +1,1137 @@ +package com.sw.plate.utils.arcface.face; + +import android.graphics.Rect; +import android.hardware.Camera; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.arcsoft.face.ErrorInfo; +import com.arcsoft.face.FaceEngine; +import com.arcsoft.face.FaceFeature; +import com.arcsoft.face.FaceInfo; +import com.arcsoft.face.ImageQualitySimilar; +import com.arcsoft.face.LivenessInfo; +import com.arcsoft.face.MaskInfo; +import com.arcsoft.face.enums.ExtractType; +import com.sw.plate.utils.L; +import com.sw.plate.utils.arcface.FaceRectTransformer; +import com.sw.plate.utils.arcface.face.constants.LivenessType; +import com.sw.plate.utils.arcface.face.constants.RequestFeatureStatus; +import com.sw.plate.utils.arcface.face.constants.RequestLivenessStatus; +import com.sw.plate.utils.arcface.face.facefilter.FaceMoveFilter; +import com.sw.plate.utils.arcface.face.facefilter.FaceRecognizeAreaFilter; +import com.sw.plate.utils.arcface.face.facefilter.FaceRecognizeFilter; +import com.sw.plate.utils.arcface.face.facefilter.FaceSizeFilter; +import com.sw.plate.utils.arcface.face.model.CompareResult; +import com.sw.plate.utils.arcface.face.model.FacePreviewInfo; +import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration; +import com.sw.plate.utils.arcface.face.model.RecognizeInfo; +import com.sw.plate.utils.arcface.faceserver.FaceServer; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; + +/** + * 人脸操作辅助类 + */ +public class FaceHelper implements FaceListener { + + private static final String TAG = "FaceHelper"; + + /** + * 识别结果的回调 + */ + private RecognizeCallback recognizeCallback; + + + /** + * 用于记录人脸识别过程信息 + */ + private ConcurrentHashMap recognizeInfoMap = new ConcurrentHashMap<>(); + + private CompositeDisposable getFeatureDelayedDisposables = new CompositeDisposable(); + private CompositeDisposable delayFaceTaskCompositeDisposable = new CompositeDisposable(); + /** + * 转换方式,用于IR活体检测 + */ + private IDualCameraFaceInfoTransformer dualCameraFaceInfoTransformer; + + /** + * 线程池正在处理任务 + */ + private static final int ERROR_BUSY = -1; + /** + * 特征提取引擎为空 + */ + private static final int ERROR_FR_ENGINE_IS_NULL = -2; + /** + * 活体检测引擎为空 + */ + private static final int ERROR_FL_ENGINE_IS_NULL = -3; + /** + * 人脸追踪引擎 + */ + private FaceEngine ftEngine; + /** + * 特征提取引擎 + */ + private FaceEngine frEngine; + /** + * 活体检测引擎 + */ + private FaceEngine flEngine; + + private Camera.Size previewSize; + + private List faceInfoList = new CopyOnWriteArrayList<>(); + private List maskInfoList = new CopyOnWriteArrayList<>(); + /** + * 特征提取线程池 + */ + private ExecutorService frExecutor; + /** + * 活体检测线程池 + */ + private ExecutorService flExecutor; + /** + * 特征提取线程队列 + */ + private LinkedBlockingQueue frThreadQueue; + /** + * 活体检测线程队列 + */ + private LinkedBlockingQueue flThreadQueue; + + private FaceRectTransformer rgbFaceRectTransformer; + private FaceRectTransformer irFaceRectTransformer; + /** + * 控制可识别区域(相对于View),若未设置,则是全部区域 + */ + private Rect recognizeArea = new Rect(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); + + private List faceRecognizeFilterList = new ArrayList<>(); + /** + * 上次应用退出时,记录的该App检测过的人脸数了 + */ + private int trackedFaceCount = 0; + /** + * 本次打开引擎后的最大faceId + */ + private int currentMaxFaceId = 0; + + /** + * 是否只检测活体 + */ + private boolean onlyDetectLiveness; + + /** + * 是否需要更新faceData + */ + private boolean needUpdateFaceData; + + /** + * 识别的配置项 + */ + private RecognizeConfiguration recognizeConfiguration; + private List currentTrackIdList = new ArrayList<>(); + private List facePreviewInfoList = new ArrayList<>(); + private Disposable timerDisposable; + + /** + * 请求获取活体检测结果,需要传入活体的参数,以下参数同 + * + * @param nv21 NV21格式的图像数据 + * @param faceInfo 人脸信息 + * @param width 图像宽度 + * @param height 图像高度 + * @param format 图像格式 + * @param livenessType 活体检测类型 + * @param waitLock + */ + public void requestFaceLiveness(byte[] nv21, FacePreviewInfo faceInfo, int width, int height, int format, LivenessType livenessType, Object waitLock) { + if (flEngine != null && flThreadQueue.remainingCapacity() > 0) { + flExecutor.execute(new FaceLivenessDetectRunnable(nv21, faceInfo, width, height, format, livenessType, waitLock)); + } else { + onFaceLivenessInfoGet(null, faceInfo.getTrackId(), ERROR_BUSY); + } + + } + + private FaceHelper(Builder builder) { + needUpdateFaceData = builder.needUpdateFaceData; + onlyDetectLiveness = builder.onlyDetectLiveness; + ftEngine = builder.ftEngine; + trackedFaceCount = builder.trackedFaceCount; + previewSize = builder.previewSize; + frEngine = builder.frEngine; + flEngine = builder.flEngine; + recognizeCallback = builder.recognizeCallback; + recognizeConfiguration = builder.recognizeConfiguration; + dualCameraFaceInfoTransformer = builder.dualCameraFaceInfoTransformer; + /* + * fr 线程队列大小 + */ + int frQueueSize = recognizeConfiguration.getMaxDetectFaces(); + if (builder.frQueueSize > 0) { + frQueueSize = builder.frQueueSize; + } else { + Log.e(TAG, "frThread num must > 0, now using default value:" + frQueueSize); + } + frThreadQueue = new LinkedBlockingQueue<>(frQueueSize); + frExecutor = new ThreadPoolExecutor(1, frQueueSize, 0, TimeUnit.MILLISECONDS, frThreadQueue, r -> { + Thread t = new Thread(r); + t.setName("frThread-" + t.getId()); + return t; + }); + + /* + * fl 线程队列大小 + */ + int flQueueSize = recognizeConfiguration.getMaxDetectFaces(); + if (builder.flQueueSize > 0) { + flQueueSize = builder.flQueueSize; + } else { + Log.e(TAG, "flThread num must > 0, now using default value:" + flQueueSize); + } + flThreadQueue = new LinkedBlockingQueue(flQueueSize); + flExecutor = new ThreadPoolExecutor(1, flQueueSize, 0, TimeUnit.MILLISECONDS, flThreadQueue, r -> { + Thread t = new Thread(r); + t.setName("flThread-" + t.getId()); + return t; + }); + if (previewSize == null) { + throw new RuntimeException("previewSize must be specified!"); + } + if (recognizeConfiguration.isEnableFaceSizeLimit()) { + // 由于目前人脸框的宽高接近一致,所以在使用时horizontalSize和verticalSize的值设置成一样 + faceRecognizeFilterList.add(new FaceSizeFilter(recognizeConfiguration.getFaceSizeLimit(), recognizeConfiguration.getFaceSizeLimit())); + } + if (recognizeConfiguration.isEnableFaceMoveLimit()) { + faceRecognizeFilterList.add(new FaceMoveFilter(recognizeConfiguration.getFaceMoveLimit())); + } + if (recognizeConfiguration.isEnableFaceAreaLimit()) { + faceRecognizeFilterList.add(new FaceRecognizeAreaFilter(recognizeArea)); + } + } + + /** + * 请求获取人脸特征数据 + * + * @param nv21 图像数据 + * @param facePreviewInfo 人脸信息 + * @param width 图像宽度 + * @param height 图像高度 + * @param format 图像格式 + */ + public void requestFaceFeature(byte[] nv21, FacePreviewInfo facePreviewInfo, int width, int height, int format) { + L.e("requestFaceFeature===frThreadQueue.remainingCapacity()=" + frThreadQueue.remainingCapacity()); + if (frEngine != null && frThreadQueue.remainingCapacity() > 0) { + frExecutor.execute(new FaceRecognizeRunnable(nv21, facePreviewInfo, width, height, format)); + } else { + onFaceFeatureInfoGet(null, facePreviewInfo.getTrackId(), ERROR_BUSY); + } + } + + public void clearFacePreviewInfoList() { + clearLeftFace(facePreviewInfoList); + if (recognizeInfoMap != null) + recognizeInfoMap.clear(); + if (getFeatureDelayedDisposables != null) { + getFeatureDelayedDisposables.clear(); + } + if (delayFaceTaskCompositeDisposable != null) { + delayFaceTaskCompositeDisposable.clear(); + } + if (faceInfoList != null) { + faceInfoList.clear(); + } + if (maskInfoList != null) { + maskInfoList.clear(); + } + if (frThreadQueue != null) { + frThreadQueue.clear(); + } + if (flThreadQueue != null) { + flThreadQueue.clear(); + } + if (faceRecognizeFilterList != null) { + faceRecognizeFilterList.clear(); + } + + if (currentTrackIdList != null) { + currentTrackIdList.clear(); + } + if (facePreviewInfoList != null) { + facePreviewInfoList.clear(); + } + } + + /** + * 释放对象 + */ + public void release() { + if (getFeatureDelayedDisposables != null) { + getFeatureDelayedDisposables.clear(); + } + if (!frExecutor.isShutdown()) { + frExecutor.shutdownNow(); + frThreadQueue.clear(); + } + if (!flExecutor.isShutdown()) { + flExecutor.shutdownNow(); + flThreadQueue.clear(); + } + if (faceInfoList != null) { + faceInfoList.clear(); + } + if (frThreadQueue != null) { + frThreadQueue.clear(); + frThreadQueue = null; + } + if (flThreadQueue != null) { + flThreadQueue.clear(); + flThreadQueue = null; + } + faceInfoList = null; + } + + /** + * 处理帧数据 + * + * @param rgbNv21 可见光相机预览回传的NV21数据 + * @param irNv21 红外相机预览回传的NV21数据 + * @param doRecognize 是否进行识别 + * @return 实时人脸处理结果,封装添加了一个trackId,trackId的获取依赖于faceId,用于记录人脸序号并保存 + */ + public List onPreviewFrame(@NonNull byte[] rgbNv21, @Nullable byte[] irNv21, boolean doRecognize) { + if (ftEngine != null) { + faceInfoList.clear(); + maskInfoList.clear(); + facePreviewInfoList.clear(); + int code = ftEngine.detectFaces(rgbNv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList); + if (code != ErrorInfo.MOK) { + onFail(new Exception("detectFaces failed,code is " + code)); + return facePreviewInfoList; + } + if (recognizeConfiguration.isKeepMaxFace()) { + keepMaxFace(faceInfoList); + } + refreshTrackId(faceInfoList); + if (faceInfoList.isEmpty()) { + return facePreviewInfoList; + } + if (!onlyDetectLiveness) { + code = ftEngine.process(rgbNv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList, + FaceEngine.ASF_MASK_DETECT); + if (code == ErrorInfo.MOK) { + code = ftEngine.getMask(maskInfoList); + if (code != ErrorInfo.MOK) { + onFail(new Exception("process getMask failed,code is " + code)); + return facePreviewInfoList; + } + } else { + onFail(new Exception("process mask failed,code is " + code)); + return facePreviewInfoList; + } + } + + for (int i = 0; i < faceInfoList.size(); i++) { + FacePreviewInfo facePreviewInfo = new FacePreviewInfo(faceInfoList.get(i), currentTrackIdList.get(i)); + if (!maskInfoList.isEmpty()) { + MaskInfo maskInfo = maskInfoList.get(i); + facePreviewInfo.setMask(maskInfo.getMask()); + } + if (rgbFaceRectTransformer != null && recognizeArea != null) { + Rect rect = rgbFaceRectTransformer.adjustRect(faceInfoList.get(i).getRect()); + Rect foreRect = rgbFaceRectTransformer.adjustRect(faceInfoList.get(i).getForeheadRect()); + facePreviewInfo.setRgbTransformedRect(rect); + facePreviewInfo.setForeRect(foreRect); + } + if (irFaceRectTransformer != null) { + FaceInfo faceInfo = faceInfoList.get(i); + if (dualCameraFaceInfoTransformer != null) { + faceInfo = dualCameraFaceInfoTransformer.transformFaceInfo(faceInfo); + } + facePreviewInfo.setFaceInfoIr(faceInfo); + facePreviewInfo.setIrTransformedRect(irFaceRectTransformer.adjustRect(faceInfo.getRect())); + } + facePreviewInfoList.add(facePreviewInfo); + } + clearLeftFace(facePreviewInfoList); + if (doRecognize) { + doRecognize(rgbNv21, irNv21, facePreviewInfoList); + } + } else { + facePreviewInfoList.clear(); + } + return facePreviewInfoList; + } + + public void setRgbFaceRectTransformer(FaceRectTransformer rgbFaceRectTransformer) { + this.rgbFaceRectTransformer = rgbFaceRectTransformer; + } + + public void setIrFaceRectTransformer(FaceRectTransformer irFaceRectTransformer) { + this.irFaceRectTransformer = irFaceRectTransformer; + } + + + /** + * 删除已经离开的人脸 + * + * @param facePreviewInfoList 人脸和trackId列表 + */ + private void clearLeftFace(List facePreviewInfoList) { + if (facePreviewInfoList == null || facePreviewInfoList.size() == 0) { + if (getFeatureDelayedDisposables != null) { + getFeatureDelayedDisposables.clear(); + } + } + Enumeration keys = recognizeInfoMap.keys(); + while (keys.hasMoreElements()) { + int key = keys.nextElement(); + boolean contained = false; + for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) { + if (facePreviewInfo.getTrackId() == key) { + contained = true; + break; + } + } + if (!contained) { + RecognizeInfo recognizeInfo = recognizeInfoMap.remove(key); + if (recognizeInfo != null) { + recognizeCallback.onNoticeChanged("leave"); + // 人脸离开时,通知特征提取线程,避免一直等待活体结果 + synchronized (recognizeInfo.getWaitLock()) { + recognizeInfo.getWaitLock().notifyAll(); + } + } + } + } + } + + private void doRecognize(byte[] rgbNv21, byte[] irNv21, List facePreviewInfoList) { + if (facePreviewInfoList != null && !facePreviewInfoList.isEmpty() && previewSize != null) { + for (FaceRecognizeFilter faceRecognizeFilter : faceRecognizeFilterList) { + faceRecognizeFilter.filter(facePreviewInfoList); + } + for (int i = 0; i < facePreviewInfoList.size(); i++) { + FacePreviewInfo facePreviewInfo = facePreviewInfoList.get(i); + if (!facePreviewInfo.isQualityPass()) { + continue; + } + //跳过mask值为MaskInfo.UNKNOWN的人脸 + if (!onlyDetectLiveness && facePreviewInfo.getMask() == MaskInfo.UNKNOWN) { + continue; + } + RecognizeInfo recognizeInfo = getRecognizeInfo(recognizeInfoMap, facePreviewInfo.getTrackId()); + int status = recognizeInfo.getRecognizeStatus(); + /* + * 在活体检测开启,在人脸识别状态不为成功或人脸活体状态不为处理中(ANALYZING)且不为处理完成(ALIVE、NOT_ALIVE)时重新进行活体检测 + */ + if (recognizeConfiguration.isEnableLiveness() && status != RequestFeatureStatus.SUCCEED) { + int liveness = recognizeInfo.getLiveness(); + if (liveness != LivenessInfo.ALIVE && liveness != LivenessInfo.NOT_ALIVE && liveness != RequestLivenessStatus.ANALYZING + || status == RequestFeatureStatus.FAILED) { + changeLiveness(facePreviewInfo.getTrackId(), RequestLivenessStatus.ANALYZING); + requestFaceLiveness( + irNv21 == null ? rgbNv21 : irNv21, + facePreviewInfo, + previewSize.width, + previewSize.height, + FaceEngine.CP_PAF_NV21, + irNv21 == null ? LivenessType.RGB : LivenessType.IR, + recognizeInfo.getWaitLock() + ); + } + } + /* + * 对于每个人脸,若状态为空或者为失败,则请求特征提取(可根据需要添加其他判断以限制特征提取次数), + * 特征提取回传的人脸特征结果在{@link FaceListener#onFaceFeatureInfoGet(FaceFeature, Integer, Integer)}中回传 + */ + if (status == RequestFeatureStatus.TO_RETRY) { + changeRecognizeStatus(facePreviewInfo.getTrackId(), RequestFeatureStatus.SEARCHING); + requestFaceFeature( + rgbNv21, facePreviewInfo, + previewSize.width, + previewSize.height, + FaceEngine.CP_PAF_NV21 + ); + } + } + } + } + + @Override + public void onFail(Exception e) { + Log.e(TAG, "onFail:" + e.getMessage()); + } + + /** + * 获取识别信息,识别信息为空则创建一个新的 + * + * @param recognizeInfoMap 存放识别信息的map + * @param trackId 人脸唯一标识 + * @return 识别信息 + */ + public RecognizeInfo getRecognizeInfo(Map recognizeInfoMap, int trackId) { + RecognizeInfo recognizeInfo = recognizeInfoMap.get(trackId); + if (recognizeInfo == null) { + recognizeInfo = new RecognizeInfo(); + recognizeInfoMap.put(trackId, recognizeInfo); + } + return recognizeInfo; + } + + @Override + public void onFaceFeatureInfoGet(@Nullable FaceFeature faceFeature, Integer trackId, Integer errorCode) { + //FR成功 + RecognizeInfo recognizeInfo = getRecognizeInfo(recognizeInfoMap, trackId); + if (faceFeature != null) { + // 人脸已离开,不用处理 + if (recognizeInfo == null) { + return; + } + //不做活体检测的情况,直接搜索 + if (!recognizeConfiguration.isEnableLiveness()) { + searchFace(faceFeature, trackId); + } + //活体检测通过,搜索特征 + else if (recognizeInfo.getLiveness() == LivenessInfo.ALIVE) { + searchFace(faceFeature, trackId); + } + //活体检测未出结果,或者非活体,等待 + else { + synchronized (recognizeInfo.getWaitLock()) { + try { + recognizeInfo.getWaitLock().wait(); + if (recognizeInfoMap.containsKey(trackId)) { + onFaceFeatureInfoGet(faceFeature, trackId, errorCode); + } + } catch (Exception e) { + Log.e(TAG, "onFaceFeatureInfoGet: 等待活体结果时退出界面会执行,正常现象,可注释异常代码块"); + e.printStackTrace(); + } + } + } + + } + //特征提取失败时,为了及时提示做个UI反馈,将name修改为"ExtractCode:${errorCode}",再重置状态 + else { + if (recognizeInfo.increaseAndGetExtractErrorRetryCount() > recognizeConfiguration.getExtractRetryCount()) { + // 在尝试最大次数后,特征提取仍然失败,则认为识别未通过 + recognizeInfo.setExtractErrorRetryCount(0); + retryRecognizeDelayed(trackId); + } else { + changeRecognizeStatus(trackId, RequestFeatureStatus.TO_RETRY); + } + } + } + + + // 定义一个Handler,用于处理延迟任务 + private Handler delayHandler = new Handler(Looper.getMainLooper()); + + /** + * 延迟 {@link RecognizeConfiguration#getLivenessFailedRetryInterval()}后,重新进行活体检测 + * + * @param trackId 人脸ID + */ + private void retryLivenessDetectDelayed(final Integer trackId) { + Log.d(TAG, "retryLivenessDetectDelayed: trackId = " + trackId + ", threadName =" + Thread.currentThread().getName()); + Log.d(TAG, "retryLivenessDetectDelayed: Interval = " + recognizeConfiguration.getLivenessFailedRetryInterval()); + + // 创建延迟任务 + Runnable delayTask = new Runnable() { + @Override + public void run() { + // 将该人脸状态置为UNKNOWN,帧回调处理时会重新进行活体检测 + changeLiveness(trackId, LivenessInfo.UNKNOWN); + // 从集合中移除这个任务 + delayFaceTaskList.remove(this); + } + }; + + // 将任务添加到集合中管理 + delayFaceTaskList.add(delayTask); + + // 发送延迟任务 + delayHandler.postDelayed(delayTask, recognizeConfiguration.getLivenessFailedRetryInterval()); + } + + /** + * 延迟 {@link RecognizeConfiguration#getRecognizeFailedRetryInterval()}后,重新进行人脸识别 + * + * @param trackId 人脸ID + */ + private void retryRecognizeDelayed(final Integer trackId) { + Log.d(TAG, "retryRecognizeDelayed: trackId = " + trackId); + changeRecognizeStatus(trackId, RequestFeatureStatus.FAILED); + + // 创建延迟任务 + Runnable delayTask = new Runnable() { + @Override + public void run() { + // 将该人脸特征提取状态置为FAILED,帧回调处理时会重新进行活体检测 + changeRecognizeStatus(trackId, RequestFeatureStatus.TO_RETRY); + // 从集合中移除这个任务 + delayFaceTaskList.remove(this); + } + }; + + // 将任务添加到集合中管理 + delayFaceTaskList.add(delayTask); + + // 发送延迟任务 + delayHandler.postDelayed(delayTask, recognizeConfiguration.getRecognizeFailedRetryInterval()); + } + + // 需要在类中定义一个集合来管理所有的延迟任务,替代原来的CompositeDisposable + private List delayFaceTaskList = new ArrayList<>(); + + @Override + public void onFaceLivenessInfoGet(@Nullable LivenessInfo livenessInfo, Integer trackId, Integer errorCode) { + Log.d(TAG, "onFaceLivenessInfoGet: trackId = " + trackId); + if (livenessInfo != null) { + int liveness = livenessInfo.getLiveness(); + Log.i(TAG, "onFaceLivenessInfoGet liveness:" + liveness); + changeLiveness(trackId, liveness); + // 非活体,重试 + if (liveness != LivenessInfo.ALIVE) { + noticeCurrentStatus("活体检测未通过"); + // 延迟 FAIL_RETRY_INTERVAL 后,将该人脸状态置为UNKNOWN,帧回调处理时会重新进行活体检测 + retryLivenessDetectDelayed(trackId); + } + } else { + RecognizeInfo recognizeInfo = getRecognizeInfo(recognizeInfoMap, trackId); + // 连续多次活体检测失败(接口调用回传值非0),将活体检测值重置为未知,会在帧回调中重新进行活体检测 + if (recognizeInfo.increaseAndGetLivenessErrorRetryCount() > recognizeConfiguration.getLivenessRetryCount()) { + recognizeInfo.setLivenessErrorRetryCount(0); + retryLivenessDetectDelayed(trackId); + } else { + changeLiveness(trackId, LivenessInfo.UNKNOWN); + } + } + } + + private void noticeCurrentStatus(String notice) { + if (recognizeCallback != null) { + recognizeCallback.onNoticeChanged(notice); + } + if (timerDisposable != null && !timerDisposable.isDisposed()) { + timerDisposable.dispose(); + } + timerDisposable = Observable.timer(1500, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(aLong -> { + if (recognizeCallback != null) { + recognizeCallback.onNoticeChanged(""); + } + }); + } + + private void searchFace(final FaceFeature faceFeature, final Integer trackId) { + CompareResult compareResult = FaceServer.getInstance().searchFaceFeature(faceFeature, frEngine); + if (compareResult == null || compareResult.getFaceEntity() == null) { + retryRecognizeDelayed(trackId); + return; + } + compareResult.setTrackId(trackId); + boolean pass = compareResult.getSimilar() > recognizeConfiguration.getSimilarThreshold(); + recognizeCallback.onRecognized(compareResult, getRecognizeInfo(recognizeInfoMap, trackId).getLiveness(), pass); + if (pass) { + setName(trackId, "识别通过"); + noticeCurrentStatus("识别通过"); + changeRecognizeStatus(trackId, RequestFeatureStatus.SUCCEED); + } else { + noticeCurrentStatus("未通过:NOT_REGISTERED"); + retryRecognizeDelayed(trackId); + } + } + + /** + * 人脸特征提取线程 + */ + public class FaceRecognizeRunnable implements Runnable { + private FaceInfo faceInfo; + private int width; + private int height; + private int format; + private Integer trackId; + private byte[] nv21Data; + private int isMask; + + /** + * 异步特征提取任务的构造函数 + * + * @param nv21Data 可见光图像数据 + * @param facePreviewInfo 人脸信息 + * @param width 图像宽度 + * @param height 图像高度 + * @param format 图像格式 + */ + private FaceRecognizeRunnable(byte[] nv21Data, FacePreviewInfo facePreviewInfo, int width, int height, int format) { + if (nv21Data == null) { + return; + } + this.nv21Data = nv21Data; + this.faceInfo = new FaceInfo(facePreviewInfo.getFaceInfoRgb()); + this.width = width; + this.height = height; + this.format = format; + this.trackId = facePreviewInfo.getTrackId(); + this.isMask = facePreviewInfo.getMask(); + } + + @Override + public void run() { + if (nv21Data != null) { + if (frEngine != null) { + if (recognizeConfiguration.isEnableImageQuality()) { + /* + * 开启人脸质量检测 + */ + ImageQualitySimilar qualitySimilar = new ImageQualitySimilar(); + int iqCode; + long iqStartTime = System.currentTimeMillis(); + synchronized (frEngine) { + iqCode = frEngine.imageQualityDetect(nv21Data, width, height, format, faceInfo, isMask, qualitySimilar); + } + Log.i(TAG, "fr iqTime:" + (System.currentTimeMillis() - iqStartTime) + "ms"); + if (iqCode == ErrorInfo.MOK) { + float quality = qualitySimilar.getScore(); + float destQuality = isMask == MaskInfo.WORN ? recognizeConfiguration.getImageQualityMaskRecognizeThreshold() : + recognizeConfiguration.getImageQualityNoMaskRecognizeThreshold(); + if (quality >= destQuality) { + extractFace(); + } else { + onFaceFail(iqCode, "fr imageQualityDetect score invalid"); + } + } else { + onFaceFail(iqCode, "fr imageQuality failed errorCode is " + iqCode); + } + } else { + extractFace(); + } + } else { + onFaceFail(ERROR_FR_ENGINE_IS_NULL, "fr failed ,frEngine is null"); + } + } + nv21Data = null; + } + + /** + * 对人脸图像进行特征提取 + */ + private void extractFace() { + long irStartTime = System.currentTimeMillis(); + FaceFeature faceFeature = new FaceFeature(); + int frCode; + synchronized (frEngine) { + /* + * 该场景为识别场景,所以参数“ExtractType”值为ExtractType.RECOGNIZE,且参数“mask”值为实际检测到的值,即isMask + */ + frCode = frEngine.extractFaceFeature(nv21Data, width, height, format, faceInfo, ExtractType.RECOGNIZE, isMask, faceFeature); + } + Log.i(TAG, "frTime:" + (System.currentTimeMillis() - irStartTime) + "ms"); + if (frCode == ErrorInfo.MOK) { + onFaceFeatureInfoGet(faceFeature, trackId, frCode); + } else { + onFaceFail(frCode, "fr failed errorCode is " + frCode); + } + } + + private void onFaceFail(int code, String errorMsg) { + onFaceFeatureInfoGet(null, trackId, code); + onFail(new Exception(errorMsg)); + } + } + + /** + * 活体检测的线程 + */ + public class FaceLivenessDetectRunnable implements Runnable { + private FaceInfo faceInfo; + private int width; + private int height; + private int format; + private Integer trackId; + private byte[] nv21Data; + private LivenessType livenessType; + private Object waitLock; + + /** + * 异步活体任务的构造函数 + * + * @param nv21Data 可见光或红外图像数据 + * @param faceInfo 可见光人脸检测得到的人脸信息 + * @param width 图像宽度 + * @param height 图像高度 + * @param format 图像格式 + * @param livenessType 活体检测类型,可以是可见光活体检测{@link LivenessType#RGB}或红外活体检测{@link LivenessType#IR} + * @param waitLock 活体检测通过后,调用该对象的notifyAll函数,通知识别线程活体已通过 + */ + private FaceLivenessDetectRunnable(byte[] nv21Data, FacePreviewInfo faceInfo, int width, int height, int format, LivenessType livenessType, Object waitLock) { + if (nv21Data == null) { + return; + } + this.nv21Data = nv21Data; + this.faceInfo = new FaceInfo(faceInfo.getFaceInfoRgb()); + this.width = width; + this.height = height; + this.format = format; + this.trackId = faceInfo.getTrackId(); + this.livenessType = livenessType; + this.waitLock = waitLock; + } + + @Override + public void run() { + if (nv21Data != null) { + if (flEngine != null) { + processLiveness(); + } else { + onProcessFail(ERROR_FL_ENGINE_IS_NULL, "fl failed ,frEngine is null"); + } + } + nv21Data = null; + } + + /** + * 执行活体检测 + */ + private void processLiveness() { + List livenessInfoList = new ArrayList<>(); + int flCode = -1; + synchronized (flEngine) { + long flStartTime = System.currentTimeMillis(); + if (livenessType == LivenessType.RGB) { + // RGB活体检测 + flCode = flEngine.process(nv21Data, width, height, format, Arrays.asList(faceInfo), FaceEngine.ASF_LIVENESS); + } else { + // IR活体检测,若有设置双目偏移,则先进行人脸框映射 + if (dualCameraFaceInfoTransformer != null) { + faceInfo = dualCameraFaceInfoTransformer.transformFaceInfo(faceInfo); + } + List faceInfoList = new ArrayList<>(); + int fdCode = flEngine.detectFaces(nv21Data, width, height, format, faceInfoList); + boolean isFaceExists = isFaceExists(faceInfoList, faceInfo); + if (fdCode == ErrorInfo.MOK && isFaceExists) { + if (needUpdateFaceData) { + /* + * 若IR人脸框有偏移,则需要对IR的人脸数据进行updateFaceData处理,再将处理后的FaceInfo信息传输给活体检测接口 + */ + flCode = flEngine.updateFaceData(nv21Data, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, + new ArrayList<>(Collections.singletonList(faceInfo))); + if (flCode == ErrorInfo.MOK) { + flCode = flEngine.processIr(nv21Data, width, height, format, Arrays.asList(faceInfo), FaceEngine.ASF_IR_LIVENESS); + } + } else { + flCode = flEngine.processIr(nv21Data, width, height, format, Arrays.asList(faceInfo), FaceEngine.ASF_IR_LIVENESS); + } + } else { + onFail(new Exception("ir detectFaces failed fdCode:" + fdCode + ",isFaceExists:" + isFaceExists)); + } + } + Log.i(TAG, "flTime:" + (System.currentTimeMillis() - flStartTime) + "ms"); + } + if (flCode == ErrorInfo.MOK) { + if (livenessType == LivenessType.RGB) { + flCode = flEngine.getLiveness(livenessInfoList); + } else { + flCode = flEngine.getIrLiveness(livenessInfoList); + } + } + + if (flCode == ErrorInfo.MOK && !livenessInfoList.isEmpty()) { + onFaceLivenessInfoGet(livenessInfoList.get(0), trackId, flCode); + if (livenessInfoList.get(0).getLiveness() == LivenessInfo.ALIVE) { + synchronized (waitLock) { + waitLock.notifyAll(); + } + } + } else { + onProcessFail(flCode, "fl failed errorCode is " + flCode); + } + } + + private void onProcessFail(int code, String msg) { + onFaceLivenessInfoGet(null, trackId, code); + onFail(new Exception(msg)); + } + } + + /** + * 如果人脸列表中有一个人脸和faceInfo相交,则认为该faceInfo可信 + * + * @param faceInfoList 人脸信息列表 + * @param faceInfo 人脸信息 + * @return 人脸信息列表中是否有人脸和传入的人脸信息相交 + */ + public static boolean isFaceExists(List faceInfoList, FaceInfo faceInfo) { + if (faceInfoList == null || faceInfoList.isEmpty() || faceInfo == null) { + return false; + } + for (FaceInfo info : faceInfoList) { + if (Rect.intersects(faceInfo.getRect(), info.getRect())) { + return true; + } + } + return false; + } + + + /** + * 刷新trackId + * + * @param ftFaceList 传入的人脸列表 + */ + private void refreshTrackId(List ftFaceList) { + currentTrackIdList.clear(); + for (FaceInfo faceInfo : ftFaceList) { + currentTrackIdList.add(faceInfo.getFaceId() + trackedFaceCount); + } + if (!ftFaceList.isEmpty()) { + currentMaxFaceId = ftFaceList.get(ftFaceList.size() - 1).getFaceId(); + } + } + + /** + * 获取当前的最大trackID,可用于退出时保存 + * + * @return 当前trackId + */ + public int getTrackedFaceCount() { + // 引擎的人脸下标从0开始,因此需要+1 + return trackedFaceCount + currentMaxFaceId + 1; + } + + /** + * 新增搜索成功的人脸 + * + * @param trackId 指定的trackId + * @param name trackId对应的人脸 + */ + public void setName(int trackId, String name) { + RecognizeInfo recognizeInfo = recognizeInfoMap.get(trackId); + if (recognizeInfo != null) { + recognizeInfo.setName(name); + } + } + + + /** + * 设置转换方式,用于IR活体检测 + * + * @param transformer 转换方式 + */ + public void setDualCameraFaceInfoTransformer(IDualCameraFaceInfoTransformer transformer) { + this.dualCameraFaceInfoTransformer = transformer; + } + + + public String getName(int trackId) { + RecognizeInfo recognizeInfo = recognizeInfoMap.get(trackId); + return recognizeInfo == null ? null : recognizeInfo.getName(); + } + + + /** + * 设置可识别区域(相对于View) + * + * @param recognizeArea 可识别区域 + */ + public void setRecognizeArea(Rect recognizeArea) { + if (recognizeArea != null) { + this.recognizeArea.set(recognizeArea); + } + } + + @IntDef(value = { + RequestFeatureStatus.FAILED, + RequestFeatureStatus.SEARCHING, + RequestFeatureStatus.SUCCEED, + RequestFeatureStatus.TO_RETRY + }) + @Retention(RetentionPolicy.SOURCE) + @interface RequestFaceFeatureStatus { + } + + @IntDef(value = { + LivenessInfo.ALIVE, + LivenessInfo.NOT_ALIVE, + LivenessInfo.UNKNOWN, + LivenessInfo.FACE_NUM_MORE_THAN_ONE, + LivenessInfo.FACE_TOO_SMALL, + LivenessInfo.FACE_ANGLE_TOO_LARGE, + LivenessInfo.FACE_BEYOND_BOUNDARY, + RequestLivenessStatus.ANALYZING + }) + @Retention(RetentionPolicy.SOURCE) + @interface RequestFaceLivenessStatus { + } + + /** + * 修改人脸识别的状态 + * + * @param trackId 根据VIDEO模式人脸检测获取的人脸的唯一标识 + * @param newStatus 新的识别状态,详见{@link RequestFeatureStatus}中的定义 + */ + public void changeRecognizeStatus(int trackId, @RequestFaceFeatureStatus int newStatus) { + getRecognizeInfo(recognizeInfoMap, trackId).setRecognizeStatus(newStatus); + } + + /** + * 修改活体活体值或活体检测状态 + * + * @param trackId 根据VIDEO模式人脸检测获取的人脸的唯一标识 + * @param newLiveness 新的活体值或活体检测状态 + */ + public void changeLiveness(int trackId, @RequestFaceLivenessStatus int newLiveness) { + getRecognizeInfo(recognizeInfoMap, trackId).setLiveness(newLiveness); + } + + /** + * 获取活体值或活体检测状态 + * + * @param trackId 根据VIDEO模式人脸检测获取的人脸的唯一标识 + * @return 活体值或活体检测状态 + */ + public Integer getLiveness(int trackId) { + return getRecognizeInfo(recognizeInfoMap, trackId).getLiveness(); + } + + /** + * 获取人脸识别状态 + * + * @param trackId 根据VIDEO模式人脸检测获取的人脸的唯一标识 + * @return 人脸识别状态 + */ + public Integer getRecognizeStatus(int trackId) { + return getRecognizeInfo(recognizeInfoMap, trackId).getRecognizeStatus(); + } + + /** + * 保留ftFaceList中最大的人脸 + * + * @param ftFaceList 人脸追踪时,一帧数据的人脸信息 + */ + private static void keepMaxFace(List ftFaceList) { + if (ftFaceList == null || ftFaceList.size() <= 1) { + return; + } + FaceInfo maxFaceInfo = ftFaceList.get(0); + for (FaceInfo faceInfo : ftFaceList) { + if (faceInfo.getRect().width() > maxFaceInfo.getRect().width()) { + maxFaceInfo = faceInfo; + } + } + ftFaceList.clear(); + ftFaceList.add(maxFaceInfo); + } + + + public static final class Builder { + private FaceEngine ftEngine; + private FaceEngine frEngine; + private FaceEngine flEngine; + private Camera.Size previewSize; + private boolean onlyDetectLiveness; + private boolean needUpdateFaceData; + private RecognizeConfiguration recognizeConfiguration; + private RecognizeCallback recognizeCallback; + private IDualCameraFaceInfoTransformer dualCameraFaceInfoTransformer; + private int frQueueSize; + private int flQueueSize; + private int trackedFaceCount; + + public Builder() { + } + + public Builder recognizeConfiguration(RecognizeConfiguration val) { + recognizeConfiguration = val; + return this; + } + + public Builder dualCameraFaceInfoTransformer(IDualCameraFaceInfoTransformer val) { + dualCameraFaceInfoTransformer = val; + return this; + } + + public Builder recognizeCallback(RecognizeCallback val) { + recognizeCallback = val; + return this; + } + + public Builder ftEngine(FaceEngine val) { + ftEngine = val; + return this; + } + + public Builder frEngine(FaceEngine val) { + frEngine = val; + return this; + } + + public Builder flEngine(FaceEngine val) { + flEngine = val; + return this; + } + + public Builder previewSize(Camera.Size val) { + previewSize = val; + return this; + } + + public Builder frQueueSize(int val) { + frQueueSize = val; + return this; + } + + public Builder flQueueSize(int val) { + flQueueSize = val; + return this; + } + + public Builder trackedFaceCount(int val) { + trackedFaceCount = val; + return this; + } + + public Builder onlyDetectLiveness(boolean val) { + onlyDetectLiveness = val; + return this; + } + + public Builder needUpdateFaceData(boolean val) { + needUpdateFaceData = val; + return this; + } + + public FaceHelper build() { + return new FaceHelper(this); + } + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/FaceListener.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/FaceListener.java new file mode 100644 index 0000000..e3080d9 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/FaceListener.java @@ -0,0 +1,37 @@ +package com.sw.plate.utils.arcface.face; + +import androidx.annotation.Nullable; + +import com.arcsoft.face.FaceFeature; +import com.arcsoft.face.LivenessInfo; + +/** + * 人脸处理回调 + */ +public interface FaceListener { + /** + * 当出现异常时执行 + * + * @param e 异常信息 + */ + void onFail(Exception e); + + + /** + * 请求人脸特征后的回调 + * + * @param faceFeature 人脸特征数据 + * @param trackId 人脸Id(相当于请求码) + * @param errorCode 错误码 + */ + void onFaceFeatureInfoGet(@Nullable FaceFeature faceFeature, Integer trackId, Integer errorCode); + + /** + * 请求活体检测后的回调 + * + * @param livenessInfo 活体检测结果 + * @param trackId 人脸Id(相当于请求码) + * @param errorCode 错误码 + */ + void onFaceLivenessInfoGet(@Nullable LivenessInfo livenessInfo, Integer trackId, Integer errorCode); +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/IDualCameraFaceInfoTransformer.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/IDualCameraFaceInfoTransformer.java new file mode 100644 index 0000000..55394fb --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/IDualCameraFaceInfoTransformer.java @@ -0,0 +1,16 @@ +package com.sw.plate.utils.arcface.face; + +import com.arcsoft.face.FaceInfo; + +/** + * 设置双目识别时,将RGB Camera帧数据检测到的人脸信息用于IR Camera帧数据活体检测时的转换方式 + */ +public interface IDualCameraFaceInfoTransformer { + /** + * 将RGB Camera帧数据检测到的人脸信息用于IR Camera帧数据活体检测时的转换方式 + * + * @param faceInfo RGB Camera帧数据检测到的人脸信息 + * @return 转换后,用于IR活体检测的FaceInfo + */ + FaceInfo transformFaceInfo(FaceInfo faceInfo); +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/RecognizeCallback.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/RecognizeCallback.java new file mode 100644 index 0000000..1694616 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/RecognizeCallback.java @@ -0,0 +1,20 @@ +package com.sw.plate.utils.arcface.face; + + +import com.sw.plate.utils.arcface.face.model.CompareResult; + +public interface RecognizeCallback { + /** + * 识别结果回调 + * + * @param compareResult 比对结果 + * @param liveness 活体值 + * @param similarPass 是否通过(依据设置的阈值) + */ + void onRecognized(CompareResult compareResult, Integer liveness, boolean similarPass); + + /** + * 提示文字变更的回调 + */ + void onNoticeChanged(String notice); +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/Test.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/Test.java new file mode 100644 index 0000000..535f2f8 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/Test.java @@ -0,0 +1,49 @@ +package com.sw.plate.utils.arcface.face; + + +import android.util.Log; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.Observable; +import io.reactivex.Observer; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; + +public class Test { + + private static final String TAG = "Test"; + private CompositeDisposable delayFaceTaskCompositeDisposable = new CompositeDisposable(); + + public void test1() { + Log.d(TAG, "test1: "); + Observable.timer(10, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + Disposable disposable; + + @Override + public void onSubscribe(Disposable d) { + disposable = d; + delayFaceTaskCompositeDisposable.add(disposable); + } + + @Override + public void onNext(Long value) { + + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onComplete() { + Log.d(TAG, "onComplete: "); + delayFaceTaskCompositeDisposable.remove(disposable); + } + }); + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/LivenessType.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/LivenessType.java new file mode 100644 index 0000000..58ba754 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/LivenessType.java @@ -0,0 +1,15 @@ +package com.sw.plate.utils.arcface.face.constants; + +/** + * 活体检测类型 + */ +public enum LivenessType { + /** + * RGB活体检测 + */ + RGB, + /** + * 红外活体检测 + */ + IR +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/RecognizeColor.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/RecognizeColor.java new file mode 100644 index 0000000..98d0497 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/RecognizeColor.java @@ -0,0 +1,22 @@ +package com.sw.plate.utils.arcface.face.constants; + +import android.graphics.Color; + +/** + * 识别过程中人脸框的颜色 + */ +public class RecognizeColor { + /** + * 未知情况的颜色 + */ + public static final int COLOR_UNKNOWN = Color.YELLOW; + /** + * 成功的颜色 + */ + public static final int COLOR_SUCCESS = Color.GREEN; + /** + * 失败的颜色 + */ + public static final int COLOR_FAILED = Color.YELLOW; + +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/RequestFeatureStatus.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/RequestFeatureStatus.java new file mode 100644 index 0000000..08137d4 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/RequestFeatureStatus.java @@ -0,0 +1,29 @@ +package com.sw.plate.utils.arcface.face.constants; + +/** + * 人脸识别中可能出现的状态 + * + * @author + */ +public @interface RequestFeatureStatus { + /** + * 默认状态 + */ + int DEFAULT = -1; + /** + * 处理中 + */ + int SEARCHING = 0; + /** + * 识别成功 + */ + int SUCCEED = 1; + /** + * 待重试 + */ + int TO_RETRY = 2; + /** + * 识别失败 + */ + int FAILED = 3; +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/RequestLivenessStatus.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/RequestLivenessStatus.java new file mode 100644 index 0000000..9ed3eb9 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/constants/RequestLivenessStatus.java @@ -0,0 +1,5 @@ +package com.sw.plate.utils.arcface.face.constants; + +public class RequestLivenessStatus { + public static final int ANALYZING = 10; +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceMoveFilter.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceMoveFilter.java new file mode 100644 index 0000000..3fef1f3 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceMoveFilter.java @@ -0,0 +1,93 @@ +package com.sw.plate.utils.arcface.face.facefilter; + +import android.graphics.Rect; + +import com.sw.plate.utils.arcface.face.model.FacePreviewInfo; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingDeque; + +/** + * 人脸移动过滤器: + * 仅保留在{@link FaceMoveFilter#CHECK_QUEUE_SIZE}帧数内,每一帧人脸的移动大小都小于{@link FaceMoveFilter#movePixels}的人脸 + */ +public class FaceMoveFilter implements FaceRecognizeFilter { + private static final String TAG = "FaceMoveFilter"; + private Map> facePositionQueueMap = new ConcurrentHashMap<>(); + private static final int CHECK_QUEUE_SIZE = 5; + private double movePixels; + + public FaceMoveFilter(double movePixels) { + this.movePixels = movePixels; + } + + @Override + public void filter(List facePreviewInfoList) { + clearFacesNotInPreview(facePreviewInfoList); + for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) { + LinkedBlockingDeque rectDeque = facePositionQueueMap.get(facePreviewInfo.getTrackId()); + if (rectDeque == null) { + rectDeque = new LinkedBlockingDeque<>(CHECK_QUEUE_SIZE); + facePositionQueueMap.put(facePreviewInfo.getTrackId(), rectDeque); + } + if (rectDeque.remainingCapacity() == 0) { + rectDeque.removeLast(); + } + rectDeque.push(facePreviewInfo.getFaceInfoRgb().getRect()); + + if (!facePreviewInfo.isQualityPass()) { + continue; + } + + boolean qualityPass = false; + if (rectDeque.size() == CHECK_QUEUE_SIZE) { + qualityPass = true; + Iterator iterator = rectDeque.iterator(); + Rect previous = iterator.next(); + while (iterator.hasNext()) { + Rect current = iterator.next(); + double distance = getDistance(current, previous); + previous = current; + if (distance > movePixels) { + qualityPass = false; + break; + } + } + } + facePreviewInfo.setQualityPass(qualityPass); + } + } + + private void clearFacesNotInPreview(List facePreviewInfo) { + Set trackIdSet = facePositionQueueMap.keySet(); + for (Integer trackId : trackIdSet) { + boolean contains = false; + for (FacePreviewInfo previewInfo : facePreviewInfo) { + if (previewInfo.getTrackId() == trackId) { + contains = true; + break; + } + } + if (!contains) { + facePositionQueueMap.remove(trackId); + } + } + } + + public static double getDistance(Rect first, Rect second) { + int firstX = first.centerX(); + int firstY = first.centerY(); + + int secondX = second.centerX(); + int secondY = second.centerY(); + + int distanceX = secondX - firstX; + int distanceY = secondY - firstY; + + return Math.sqrt(distanceX * distanceX + distanceY * distanceY); + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceRecognizeAreaFilter.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceRecognizeAreaFilter.java new file mode 100644 index 0000000..e713432 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceRecognizeAreaFilter.java @@ -0,0 +1,31 @@ +package com.sw.plate.utils.arcface.face.facefilter; + + +import android.graphics.Rect; + +import com.sw.plate.utils.arcface.face.model.FacePreviewInfo; + +import java.util.List; + +/** + * 人脸识别区域过滤器: + * 仅保留人脸区域在{@link FaceRecognizeAreaFilter#validArea}中的人脸。(基于View位置判断) + */ +public class FaceRecognizeAreaFilter implements FaceRecognizeFilter { + private static final String TAG = "FaceRecognizeAreaFilter"; + private Rect validArea; + + public FaceRecognizeAreaFilter(Rect validArea) { + this.validArea = validArea; + } + + @Override + public void filter(List facePreviewInfoList) { + for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) { + if (!facePreviewInfo.isQualityPass()) { + continue; + } + facePreviewInfo.setQualityPass(validArea.contains(facePreviewInfo.getRgbTransformedRect())); + } + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceRecognizeFilter.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceRecognizeFilter.java new file mode 100644 index 0000000..0c80f22 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceRecognizeFilter.java @@ -0,0 +1,13 @@ +package com.sw.plate.utils.arcface.face.facefilter; + + +import com.sw.plate.utils.arcface.face.model.FacePreviewInfo; + +import java.util.List; + +/** + * 人脸识别过滤器,仅保留满足条件的人脸,(只有满足条件的人脸才进行后续的活体检测、人脸识别操作) + */ +public interface FaceRecognizeFilter { + void filter(List facePreviewInfoList); +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceSizeFilter.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceSizeFilter.java new file mode 100644 index 0000000..ecebdab --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/facefilter/FaceSizeFilter.java @@ -0,0 +1,39 @@ +package com.sw.plate.utils.arcface.face.facefilter; + +import android.graphics.Rect; + +import com.sw.plate.utils.arcface.face.model.FacePreviewInfo; + +import java.util.List; + +/** + * 人脸尺寸过滤器: + * 仅保留人脸宽度大于{@link FaceSizeFilter#horizontalSize},且人脸高度大于{@link FaceSizeFilter#verticalSize}的人脸。 + */ +public class FaceSizeFilter implements FaceRecognizeFilter { + private int horizontalSize; + private int verticalSize; + + private static final String TAG = "FaceSizeFilter"; + + public FaceSizeFilter(int horizontalSize, int verticalSize) { + this.horizontalSize = horizontalSize; + this.verticalSize = verticalSize; + } + + @Override + public void filter(List facePreviewInfoList) { + for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) { + if (!facePreviewInfo.isQualityPass()) { + continue; + } + if (facePreviewInfo.getFaceInfoRgb() != null) { + Rect rgbRect = facePreviewInfo.getFaceInfoRgb().getRect(); + Rect irRect = facePreviewInfo.getFaceInfoIr() == null ? null : facePreviewInfo.getFaceInfoIr().getRect(); + boolean rgbRectValid = rgbRect == null || (rgbRect.width() > horizontalSize && rgbRect.height() > verticalSize); + boolean irRectValid = irRect == null || (irRect.width() > horizontalSize && irRect.height() > verticalSize); + facePreviewInfo.setQualityPass(rgbRectValid && irRectValid); + } + } + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/CompareResult.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/CompareResult.java new file mode 100644 index 0000000..2d33f8b --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/CompareResult.java @@ -0,0 +1,64 @@ +package com.sw.plate.utils.arcface.face.model; + + +import com.sw.plate.utils.arcface.facedb.entity.FaceEntity; + +public class CompareResult { + private FaceEntity faceEntity; + private float similar; + private int trackId; + private int compareCode; + private long cost; + + public CompareResult(FaceEntity faceEntity, float similar) { + this.faceEntity = faceEntity; + this.similar = similar; + } + + public CompareResult(FaceEntity faceEntity, float similar, int compareCode, long cost) { + this.faceEntity = faceEntity; + this.similar = similar; + this.compareCode = compareCode; + this.cost = cost; + } + + public FaceEntity getFaceEntity() { + return faceEntity; + } + + public void setFaceEntity(FaceEntity faceEntity) { + this.faceEntity = faceEntity; + } + + public float getSimilar() { + return similar; + } + + public void setSimilar(float similar) { + this.similar = similar; + } + + public int getTrackId() { + return trackId; + } + + public void setTrackId(int trackId) { + this.trackId = trackId; + } + + public int getCompareCode() { + return compareCode; + } + + public void setCompareCode(int compareCode) { + this.compareCode = compareCode; + } + + public long getCost() { + return cost; + } + + public void setCost(long cost) { + this.cost = cost; + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/FacePreviewInfo.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/FacePreviewInfo.java new file mode 100644 index 0000000..8f0b045 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/FacePreviewInfo.java @@ -0,0 +1,152 @@ +package com.sw.plate.utils.arcface.face.model; + +import android.graphics.Rect; + +import com.arcsoft.face.FaceInfo; +import com.arcsoft.face.LivenessInfo; + +/** + * 人脸追踪时的信息 + */ +public class FacePreviewInfo { + /** + * RGB人脸信息,包括人脸框和人脸角度 + */ + private FaceInfo faceInfoRgb; + /** + * IR人脸信息,包括人脸框和人脸角度 + */ + private FaceInfo faceInfoIr; + /** + * 可见光成像对应的用于FaceRectView绘制的Rect + */ + private Rect rgbTransformedRect; + /** + * 红外成像对应的用于FaceRectView绘制的Rect + */ + private Rect irTransformedRect; + private int rgbLiveness = LivenessInfo.UNKNOWN; + private int irLiveness = LivenessInfo.UNKNOWN; + private float imageQuality = 0f; + /** + * 识别区域是否合法 + */ + private boolean recognizeAreaValid; + /** + * 基于{@link FaceInfo#getFaceId()}的一个偏移值,可理解为SDK截至目前检测到的人次,唯一性同faceId + */ + private int trackId; + /** + * 整体质量是否通过,包括人脸大小、角度、移动速度等 + */ + private boolean qualityPass = true; + + /** + * 是否戴口罩 + */ + private int mask; + + private Rect foreRect; + + public Rect getForeRect() { + return foreRect; + } + + public void setForeRect(Rect foreRect) { + this.foreRect = foreRect; + } + + public FacePreviewInfo(FaceInfo faceInfoRgb, int trackId) { + this.faceInfoRgb = faceInfoRgb; + this.trackId = trackId; + } + + public FaceInfo getFaceInfoRgb() { + return faceInfoRgb; + } + + public void setFaceInfoRgb(FaceInfo faceInfoRgb) { + this.faceInfoRgb = faceInfoRgb; + } + + + public int getTrackId() { + return trackId; + } + + public void setTrackId(int trackId) { + this.trackId = trackId; + } + + public void setRgbTransformedRect(Rect rgbTransformedRect) { + this.rgbTransformedRect = rgbTransformedRect; + } + + public void setIrTransformedRect(Rect irTransformedRect) { + this.irTransformedRect = irTransformedRect; + } + + public Rect getRgbTransformedRect() { + return rgbTransformedRect; + } + + public Rect getIrTransformedRect() { + return irTransformedRect; + } + + public boolean isRecognizeAreaValid() { + return recognizeAreaValid; + } + + public void setRecognizeAreaValid(boolean recognizeAreaValid) { + this.recognizeAreaValid = recognizeAreaValid; + } + + public void setFaceInfoIr(FaceInfo faceInfoIr) { + this.faceInfoIr = faceInfoIr; + } + + public FaceInfo getFaceInfoIr() { + return faceInfoIr; + } + + public int getRgbLiveness() { + return rgbLiveness; + } + + public void setRgbLiveness(int rgbLiveness) { + this.rgbLiveness = rgbLiveness; + } + + public int getIrLiveness() { + return irLiveness; + } + + public void setIrLiveness(int irLiveness) { + this.irLiveness = irLiveness; + } + + public void setImageQuality(float imageQuality) { + this.imageQuality = imageQuality; + } + + public float getImageQuality() { + return imageQuality; + } + + public boolean isQualityPass() { + return qualityPass; + } + + public void setQualityPass(boolean qualityPass) { + this.qualityPass = qualityPass; + } + + public int getMask() { + return mask; + } + + public void setMask(int mask) { + this.mask = mask; + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/RecognizeConfiguration.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/RecognizeConfiguration.java new file mode 100644 index 0000000..ee22a9b --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/RecognizeConfiguration.java @@ -0,0 +1,302 @@ +package com.sw.plate.utils.arcface.face.model; + +import com.arcsoft.face.LivenessParam; +import com.sw.plate.utils.arcface.ConfigUtil; + +/** + * 识别相关的配置项 + */ +public class RecognizeConfiguration { + /** + * 产生特征提取失败示语的特征提取次数(小于该值不提示) + */ + private int extractRetryCount; + /** + * 产生活体检测失败示语的活体检测次数(小于该值不提示) + */ + private int livenessRetryCount; + /** + * 最大人脸检测数量 + */ + private int maxDetectFaces; + /** + * 识别阈值 + */ + private float similarThreshold; + /** + * 图像质量检测阈值:适用于不戴口罩且人脸识别场景 + */ + private float imageQualityNoMaskRecognizeThreshold; + /** + * 图像质量检测阈值:适用于戴口罩且人脸识别场景 + */ + private float imageQualityMaskRecognizeThreshold; + /** + * 识别失败重试间隔 + */ + private int recognizeFailedRetryInterval; + /** + * 活体检测未通过重试间隔 + */ + private int livenessFailedRetryInterval; + /** + * 启用活体 + */ + private boolean enableLiveness; + /** + * 启用图像质量检测 + */ + private boolean enableImageQuality; + /** + * 识别区域限制 + */ + private boolean enableFaceAreaLimit; + /** + * 仅识别最大人脸 + */ + private boolean keepMaxFace; + /** + * 活体阈值设置 + */ + private LivenessParam livenessParam; + + + /** + * 启用人脸边长限制 + */ + private boolean enableFaceSizeLimit = false; + /** + * 启用人脸移动限制 + */ + private boolean enableFaceMoveLimit = false; + /** + * 人脸边长限制值 + */ + private int faceSizeLimit = 0; + /** + * 人脸上下针移动限制值 + */ + private int faceMoveLimit = 0; + + + public RecognizeConfiguration(Builder builder) { + this.extractRetryCount = builder.extractRetryCount; + this.livenessRetryCount = builder.livenessRetryCount; + this.livenessFailedRetryInterval = builder.livenessFailedRetryInterval; + this.maxDetectFaces = builder.maxDetectFaces; + this.similarThreshold = builder.similarThreshold; + this.imageQualityNoMaskRecognizeThreshold = builder.imageQualityNoMaskRecognizeThreshold; + this.imageQualityMaskRecognizeThreshold = builder.imageQualityMaskRecognizeThreshold; + this.enableLiveness = builder.enableLiveness; + this.enableImageQuality = builder.enableImageQuality; + this.enableFaceAreaLimit = builder.enableFaceAreaLimit; + this.keepMaxFace = builder.keepMaxFace; + this.recognizeFailedRetryInterval = builder.recognizeFailedRetryInterval; + this.livenessParam = builder.livenessParam; + this.enableFaceSizeLimit = builder.enableFaceSizeLimit; + this.enableFaceMoveLimit = builder.enableFaceMoveLimit; + this.faceSizeLimit = builder.faceSizeLimit; + this.faceMoveLimit = builder.faceMoveLimit; + } + + //TODO: demo不实现所有配置,若以下项也需要进行自定义配置,可参考其他配置项实现 + public static class Builder { + private int extractRetryCount = 3; + private int livenessRetryCount = 3; + private int maxDetectFaces = 3; + private int recognizeFailedRetryInterval = 0; + private int livenessFailedRetryInterval = 0; + private float similarThreshold = 0.8f; + private float imageQualityNoMaskRecognizeThreshold = ConfigUtil.IMAGE_QUALITY_NO_MASK_RECOGNIZE_THRESHOLD; + private float imageQualityMaskRecognizeThreshold = ConfigUtil.IMAGE_QUALITY_MASK_RECOGNIZE_THRESHOLD; + private boolean enableLiveness = false; + private boolean enableFaceAreaLimit = false; + private boolean enableImageQuality = false; + private boolean enableFaceSizeLimit = false; + private boolean enableFaceMoveLimit = false; + private int faceSizeLimit = 0; + private int faceMoveLimit = 0; + private boolean keepMaxFace = false; + private LivenessParam livenessParam; + + public Builder recognizeFailedRetryInterval(int val) { + this.recognizeFailedRetryInterval = val; + return this; + } + + public Builder livenessFailedRetryInterval(int val) { + this.livenessFailedRetryInterval = val; + return this; + } + + public Builder extractRetryCount(int val) { + this.extractRetryCount = val; + return this; + } + + public Builder livenessRetryCount(int val) { + this.livenessRetryCount = val; + return this; + } + + public Builder maxDetectFaces(int val) { + this.maxDetectFaces = val; + return this; + } + + public Builder similarThreshold(float val) { + this.similarThreshold = val; + return this; + } + + public Builder imageQualityNoMaskRecognizeThreshold(float val) { + this.imageQualityNoMaskRecognizeThreshold = val; + return this; + } + + public Builder imageQualityMaskRecognizeThreshold(float val) { + this.imageQualityMaskRecognizeThreshold = val; + return this; + } + + public Builder enableLiveness(boolean val) { + this.enableLiveness = val; + return this; + } + + + public Builder enableImageQuality(boolean val) { + this.enableImageQuality = val; + return this; + } + + public Builder enableFaceAreaLimit(boolean val) { + this.enableFaceAreaLimit = val; + return this; + } + + public Builder enableFaceSizeLimit(boolean val) { + this.enableFaceSizeLimit = val; + return this; + } + + public Builder enableFaceMoveLimit(boolean val) { + this.enableFaceMoveLimit = val; + return this; + } + + public Builder faceSizeLimit(int val) { + this.faceSizeLimit = val; + return this; + } + + public Builder faceMoveLimit(int val) { + this.faceMoveLimit = val; + return this; + } + + public Builder keepMaxFace(boolean val) { + this.keepMaxFace = val; + return this; + } + + public Builder livenessParam(LivenessParam val) { + this.livenessParam = val; + return this; + } + + + public RecognizeConfiguration build() { + return new RecognizeConfiguration(this); + } + } + + public float getImageQualityNoMaskRecognizeThreshold() { + return imageQualityNoMaskRecognizeThreshold; + } + + public float getImageQualityMaskRecognizeThreshold() { + return imageQualityMaskRecognizeThreshold; + } + + public boolean isEnableImageQuality() { + return enableImageQuality; + } + + public boolean isEnableFaceAreaLimit() { + return enableFaceAreaLimit; + } + + public LivenessParam getLivenessParam() { + return livenessParam; + } + + public int getExtractRetryCount() { + return extractRetryCount; + } + + public int getLivenessRetryCount() { + return livenessRetryCount; + } + + public int getMaxDetectFaces() { + return maxDetectFaces; + } + + public float getSimilarThreshold() { + return similarThreshold; + } + + public boolean isEnableLiveness() { + return enableLiveness; + } + + + public int getRecognizeFailedRetryInterval() { + return recognizeFailedRetryInterval; + } + + public int getLivenessFailedRetryInterval() { + return livenessFailedRetryInterval; + } + + public boolean isKeepMaxFace() { + return keepMaxFace; + } + + public boolean isEnableFaceSizeLimit() { + return enableFaceSizeLimit; + } + + public boolean isEnableFaceMoveLimit() { + return enableFaceMoveLimit; + } + + public int getFaceSizeLimit() { + return faceSizeLimit; + } + + public int getFaceMoveLimit() { + return faceMoveLimit; + } + + @Override + public String toString() { + return + "extractRetryCount: " + extractRetryCount + "\r\n" + + "similarThreshold: " + similarThreshold + "\r\n" + + "recognizeFailedRetryInterval: " + recognizeFailedRetryInterval + "\r\n" + + + "keepMaxFace: " + keepMaxFace + "\r\n" + + "maxDetectFaces: " + maxDetectFaces + "\r\n" + + + "enableImageQuality: " + enableImageQuality + "\r\n" + + "imageQualityNoMaskRecognizeThreshold: " + imageQualityNoMaskRecognizeThreshold + "\r\n" + + "imageQualityMaskRecognizeThreshold: " + imageQualityMaskRecognizeThreshold + "\r\n" + + + "enableLiveness: " + enableLiveness + "\r\n" + + "livenessRetryCount: " + livenessRetryCount + "\r\n" + + "livenessParams: " + (livenessParam == null ? null : (livenessParam.getRgbThreshold() + "," + livenessParam.getIrThreshold())); + + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/RecognizeInfo.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/RecognizeInfo.java new file mode 100644 index 0000000..b49e421 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/face/model/RecognizeInfo.java @@ -0,0 +1,86 @@ +package com.sw.plate.utils.arcface.face.model; + +import com.arcsoft.face.LivenessInfo; +import com.sw.plate.utils.arcface.face.constants.RequestFeatureStatus; + +/** + * 单个人脸(faceId)识别过程中的信息 + */ +public class RecognizeInfo { + /** + * 用于记录人脸识别相关状态 + */ + private int recognizeStatus = RequestFeatureStatus.TO_RETRY; + /** + * 用于记录人脸特征提取出错重试次数 + */ + private int extractErrorRetryCount; + /** + * 用于存储活体值 + */ + private int liveness = LivenessInfo.UNKNOWN; + /** + * 用于存储活体检测出错重试次数 + */ + private int livenessErrorRetryCount; + /** + * 用户姓名,用于显示 + */ + private String name; + /** + * 特征等活体的lock + */ + private Object waitLock = new Object(); + + public int getRecognizeStatus() { + return recognizeStatus; + } + + public void setRecognizeStatus(int recognizeStatus) { + this.recognizeStatus = recognizeStatus; + } + + public void setLiveness(int liveness) { + this.liveness = liveness; + } + + public int increaseAndGetExtractErrorRetryCount() { + return ++extractErrorRetryCount; + } + + public int getLiveness() { + return liveness; + } + + public int increaseAndGetLivenessErrorRetryCount() { + return ++livenessErrorRetryCount; + } + + public void setExtractErrorRetryCount(int extractErrorRetryCount) { + this.extractErrorRetryCount = extractErrorRetryCount; + } + + public void setLivenessErrorRetryCount(int livenessErrorRetryCount) { + this.livenessErrorRetryCount = livenessErrorRetryCount; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Object getWaitLock() { + return waitLock; + } + + public int getExtractErrorRetryCount() { + return extractErrorRetryCount; + } + + public int getLivenessErrorRetryCount() { + return livenessErrorRetryCount; + } +} \ No newline at end of file diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/FaceDatabase.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/FaceDatabase.java new file mode 100644 index 0000000..ddb985f --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/FaceDatabase.java @@ -0,0 +1,31 @@ +package com.sw.plate.utils.arcface.facedb; + +import android.content.Context; + +import androidx.room.Database; +import androidx.room.Room; +import androidx.room.RoomDatabase; + +import com.sw.plate.utils.arcface.facedb.dao.FaceDao; +import com.sw.plate.utils.arcface.facedb.entity.FaceEntity; + +@Database(entities = {FaceEntity.class}, version = 1, exportSchema = false) +public abstract class FaceDatabase extends RoomDatabase { + public abstract FaceDao faceDao(); + + private static volatile FaceDatabase faceDatabase = null; + + public static FaceDatabase getInstance(Context context) { + if (faceDatabase == null) { + synchronized (FaceDatabase.class) { + if (faceDatabase == null) { + faceDatabase = Room.databaseBuilder(context, FaceDatabase.class, + context.getDatabasePath("faceDB.db").getPath() +// context.getExternalFilesDir("database") + File.separator + "faceDB.db" + ).build(); + } + } + } + return faceDatabase; + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/dao/FaceDao.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/dao/FaceDao.java new file mode 100644 index 0000000..4b3a682 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/dao/FaceDao.java @@ -0,0 +1,91 @@ +package com.sw.plate.utils.arcface.facedb.dao; + +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; +import androidx.room.Update; + +import com.sw.plate.utils.arcface.facedb.entity.FaceEntity; + +import java.util.List; + +@Dao +public interface FaceDao { + /** + * 获取库中所有已注册人脸 + * + * @return 所有已注册人脸 + */ + @Query("SELECT * FROM face") + List getAllFaces(); + + /** + * 分页获取库中的人脸 + * + * @param start 起始下标 + * @param size 单次获取的长度 + * @return 从下标为start开始的size个已注册人脸 + */ + @Query("SELECT * FROM face order by faceId desc limit :start,:size ") + List getFaces(int start, int size); + + /** + * 更新已注册的人脸信息 + * + * @param faceEntity 已注册的人脸信息 + * @return + */ + @Update + int updateFaceEntity(FaceEntity faceEntity); + + /** + * 删除人脸 + * + * @param faceEntity 已注册的人脸信息 + * @return + */ + @Delete + int deleteFace(FaceEntity faceEntity); + + /** + * @return 该用户已注册人脸 + */ + @Query("DELETE from face WHERE user_name = :userName") + int deleteFaceById(String userName); + + /** + * 删除所有已注册的人脸 + * + * @return + */ + @Query("DELETE from face") + int deleteAll(); + + /** + * 插入一个人脸入库 + * + * @param faceEntity + * @return + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + Long insert(FaceEntity faceEntity); + + @Insert(onConflict = OnConflictStrategy.IGNORE) + List insert(List items); + + /** + * 获取已注册的人脸数 + * + * @return + */ + @Query("SELECT COUNT(1) from face") + int getFaceCount(); + + @Query("SELECT * FROM face WHERE faceId = :faceId limit 1") + FaceEntity queryByFaceId(int faceId); + + @Query("UPDATE sqlite_sequence SET seq = 0 WHERE name ='face'") + void resetId(); +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/entity/FaceEntity.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/entity/FaceEntity.java new file mode 100644 index 0000000..aac56cb --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/entity/FaceEntity.java @@ -0,0 +1,159 @@ +package com.sw.plate.utils.arcface.facedb.entity; + +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.room.ColumnInfo; +import androidx.room.Entity; +import androidx.room.PrimaryKey; + +import java.util.Arrays; +import java.util.Objects; + +/** + * 人脸库中的单挑人脸记录 + */ +@Entity( + tableName = "face" +) +public class FaceEntity implements Parcelable { + /** + * 人脸id,主键 + */ + @PrimaryKey(autoGenerate = true) + private long faceId; + /** + * 用户名称 + */ + @ColumnInfo(name = "user_name") + private String userName; + /** + * 图片路径 + */ + @ColumnInfo(name = "image_path") + private String imagePath; + /** + * 人脸特征数据 + */ + @ColumnInfo(name = "feature_data") + private byte[] featureData; + /** + * 注册时间 + */ + @ColumnInfo(name = "register_time") + private long registerTime; + + + public FaceEntity(String userName, String imagePath, byte[] featureData) { + this.userName = userName; + this.imagePath = imagePath; + this.featureData = featureData; + registerTime = System.currentTimeMillis(); + } + + public FaceEntity(FaceEntity faceEntity) { + this.faceId = faceEntity.faceId; + this.userName = faceEntity.userName; + this.imagePath = faceEntity.imagePath; + this.featureData = faceEntity.featureData; + this.registerTime = faceEntity.registerTime; + } + + + protected FaceEntity(Parcel in) { + faceId = in.readLong(); + registerTime = in.readLong(); + userName = in.readString(); + imagePath = in.readString(); + featureData = in.createByteArray(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public FaceEntity createFromParcel(Parcel in) { + return new FaceEntity(in); + } + + @Override + public FaceEntity[] newArray(int size) { + return new FaceEntity[size]; + } + }; + + public long getFaceId() { + return faceId; + } + + public void setFaceId(long faceId) { + this.faceId = faceId; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getImagePath() { + return imagePath; + } + + public void setImagePath(String imagePath) { + this.imagePath = imagePath; + } + + public byte[] getFeatureData() { + return featureData; + } + + public void setFeatureData(byte[] featureData) { + this.featureData = featureData; + } + + public long getRegisterTime() { + return registerTime; + } + + public void setRegisterTime(long registerTime) { + this.registerTime = registerTime; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(faceId); + dest.writeLong(registerTime); + dest.writeString(userName); + dest.writeString(imagePath); + dest.writeByteArray(featureData); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FaceEntity that = (FaceEntity) o; + return faceId == that.faceId && + registerTime == that.registerTime && + userName.equals(that.userName) && + imagePath.equals(that.imagePath) && + Arrays.equals(featureData, that.featureData); + } + + @Override + public int hashCode() { + int result = Objects.hash(faceId, registerTime, userName, imagePath); + result = 31 * result + Arrays.hashCode(featureData); + return result; + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/faceserver/FaceServer.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/faceserver/FaceServer.java new file mode 100644 index 0000000..da36a0e --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/faceserver/FaceServer.java @@ -0,0 +1,624 @@ +package com.sw.plate.utils.arcface.faceserver; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.util.Log; + +import com.arcsoft.face.ErrorInfo; +import com.arcsoft.face.FaceEngine; +import com.arcsoft.face.FaceFeature; +import com.arcsoft.face.FaceFeatureInfo; +import com.arcsoft.face.FaceInfo; +import com.arcsoft.face.MaskInfo; +import com.arcsoft.face.SearchResult; +import com.arcsoft.face.enums.DetectFaceOrientPriority; +import com.arcsoft.face.enums.DetectMode; +import com.arcsoft.face.enums.ExtractType; +import com.arcsoft.imageutil.ArcSoftImageFormat; +import com.arcsoft.imageutil.ArcSoftImageUtil; +import com.arcsoft.imageutil.ArcSoftImageUtilError; +import com.arcsoft.imageutil.ArcSoftRotateDegree; +import com.sw.plate.App; +import com.sw.plate.utils.arcface.ErrorCodeUtil; +import com.sw.plate.utils.arcface.ImageUtil; +import com.sw.plate.utils.arcface.face.model.CompareResult; +import com.sw.plate.utils.arcface.face.model.FacePreviewInfo; +import com.sw.plate.utils.arcface.facedb.FaceDatabase; +import com.sw.plate.utils.arcface.facedb.entity.FaceEntity; +import com.sw.plate.utils.arcface.model.UserFaceInfo; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import io.reactivex.Observable; +import io.reactivex.ObservableOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +/** + * 人脸库操作类,包含注册和搜索 + */ +public class FaceServer { + private static final String TAG = "FaceServer"; + private static FaceEngine faceEngine = null; + private static volatile FaceServer faceServer = null; + private List faceRegisterInfoList; + private String imageRootPath; + /** + * 最大注册人脸数 + */ + private static final int MAX_REGISTER_FACE_COUNT = 30000; + + private FaceServer() { + faceRegisterInfoList = new ArrayList<>(); + } + + public static FaceServer getInstance() { + if (faceServer == null) { + synchronized (FaceServer.class) { + if (faceServer == null) { + faceServer = new FaceServer(); + } + } + } + return faceServer; + } + + public interface OnInitFinishedCallback { + void onFinished(int faceCount); + } + + public void init(Context context) { + init(context, null); + } + + public synchronized void init(Context context, OnInitFinishedCallback onInitFinishedCallback) { + if (faceEngine == null && context != null) { + faceEngine = new FaceEngine(); + int engineCode = faceEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_ALL_OUT, + 1, FaceEngine.ASF_FACE_RECOGNITION | FaceEngine.ASF_FACE_DETECT | FaceEngine.ASF_MASK_DETECT); + if (engineCode == ErrorInfo.MOK) { + initFaceList(context, null, onInitFinishedCallback, false); + } else { + faceEngine = null; + Log.e(TAG, "init: failed! code = " + engineCode); + } + } + if (faceRegisterInfoList != null && onInitFinishedCallback != null) { + onInitFinishedCallback.onFinished(faceRegisterInfoList.size()); + } + } + + /** + * 销毁 + */ + public synchronized void release() { + if (faceRegisterInfoList != null) { + faceRegisterInfoList.clear(); + faceRegisterInfoList = null; + } + if (faceEngine != null) { + synchronized (faceEngine) { + faceEngine.unInit(); + } + faceEngine = null; + } + faceServer = null; + } + + /** + * 初始化人脸特征数据以及人脸特征数据对应的注册图 + * + * @param context 上下文对象 + * @param faceEngine 指定FaceEngine + * @param onInitFinishedCallback 加载完成的回调 + * @param recognize 是否处于人脸识别流程 + */ + public void initFaceList(final Context context, FaceEngine faceEngine, final OnInitFinishedCallback onInitFinishedCallback, boolean recognize) { + Disposable disposable = Observable.create((ObservableOnSubscribe) emitter -> { + if (recognize) { + List faceEntityList = FaceDatabase.getInstance(context).faceDao().getAllFaces(); + registerFaceFeatureInfoListFromDb(faceEngine, faceEntityList); + emitter.onNext(faceEntityList.size()); + } else { + faceRegisterInfoList = FaceDatabase.getInstance(context).faceDao().getAllFaces(); + emitter.onNext(faceRegisterInfoList == null ? 0 : faceRegisterInfoList.size()); + } + emitter.onComplete(); + }).subscribeOn(Schedulers.io()) + .unsubscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(size -> { + if (onInitFinishedCallback != null) { + onInitFinishedCallback.onFinished(size); + } + }); + } + + public synchronized void removeOneFace(FaceEntity faceEntity) { + if (faceRegisterInfoList != null) { + faceRegisterInfoList.remove(faceEntity); + } + } + + + public synchronized void removeFaceById(String id) { + Iterator iterator = faceRegisterInfoList.iterator(); + while (iterator.hasNext()) { + FaceEntity next = iterator.next(); + if (id.equals(next.getUserName())) { + iterator.remove(); + } + } + } + + public synchronized void addUserFace(FaceEntity faceEntity) { + faceRegisterInfoList.add(faceEntity); + } + + + @SuppressLint("CheckResult") + public synchronized int clearAllFaces() { + if (faceRegisterInfoList != null) { + faceRegisterInfoList.clear(); + } + if (faceEngine != null) { + faceEngine.removeFaceFeature(-1); + } + Context applicationContext = App.getContext(); + int deleteSize = FaceDatabase.getInstance(applicationContext).faceDao().deleteAll(); + File imgDir = new File(getImageDir()); + File[] files = imgDir.listFiles(); + if (files != null && files.length > 0) { + for (File file : files) { + file.delete(); + } + } + return deleteSize; + } + + /** + * 用于预览时注册人脸 + * + * @param context 上下文对象 + * @param nv21 NV21数据 + * @param width NV21宽度 + * @param height NV21高度 + * @param faceInfo {@link FaceEngine#detectFaces(byte[], int, int, int, List)}获取的人脸信息 + * @param name 保存的名字,若为空则使用时间戳 + * @param frEngine 添加人脸数据,用于后续{@link FaceEngine#searchFaceFeature(FaceFeature)} + * @param registerFaceEngine 用于{@link FaceEngine#extractFaceFeature(byte[], int, int, int, FaceInfo, ExtractType, int, FaceFeature)}注册人脸到本地数据库 + * @return 是否注册成功 + */ + public boolean registerNv21(Context context, byte[] nv21, int width, int height, FacePreviewInfo faceInfo, String name, + FaceEngine frEngine, FaceEngine registerFaceEngine) { + if (registerFaceEngine == null || context == null || nv21 == null || width % 4 != 0 || nv21.length != width * height * 3 / 2) { + Log.e(TAG, "registerNv21: invalid params"); + return false; + } + FaceFeature faceFeature = new FaceFeature(); + int code; + /* + * 特征提取,注册人脸时extractType值为ExtractType.REGISTER,mask的值为MaskInfo.NOT_WORN + */ + synchronized (registerFaceEngine) { + code = registerFaceEngine.extractFaceFeature(nv21, width, height, FaceEngine.CP_PAF_NV21, faceInfo.getFaceInfoRgb(), + ExtractType.REGISTER, MaskInfo.NOT_WORN, faceFeature); + } + if (code != ErrorInfo.MOK) { + Log.e(TAG, "registerNv21: extractFaceFeature failed , code is " + code); + return false; + } else { + /* + * 1.保存注册结果(注册图、特征数据) + * 2.为了美观,扩大rect截取注册图 + */ + Rect cropRect = getBestRect(width, height, faceInfo.getFaceInfoRgb().getRect()); + if (cropRect == null) { + Log.e(TAG, "registerNv21: cropRect is null!"); + return false; + } + + cropRect.left &= ~3; + cropRect.top &= ~3; + cropRect.right &= ~3; + cropRect.bottom &= ~3; + + // 创建一个头像的Bitmap,存放旋转结果图 + Bitmap headBmp = getHeadImage(nv21, width, height, faceInfo.getFaceInfoRgb().getOrient(), cropRect, ArcSoftImageFormat.NV21); + String imgPath = getImagePath(name); + try { + FileOutputStream fos = new FileOutputStream(imgPath); + headBmp.compress(Bitmap.CompressFormat.JPEG, 100, fos); + fos.close(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + FaceEntity faceEntity = new FaceEntity(name, imgPath, faceFeature.getFeatureData()); + long faceId = FaceDatabase.getInstance(context).faceDao().insert(faceEntity); + faceEntity.setFaceId(faceId); + registerFaceFeatureInfoFromDb(faceEntity, frEngine); + return true; + } + } + + + public UserFaceInfo getUserInfo(Context context, byte[] nv21, int width, int height, FacePreviewInfo faceInfo, String name, + FaceEngine frEngine, FaceEngine registerFaceEngine) { + if (registerFaceEngine == null || context == null || nv21 == null || width % 4 != 0 || nv21.length != width * height * 3 / 2) { + Log.e(TAG, "registerNv21: invalid params"); + return null; + } + FaceFeature faceFeature = new FaceFeature(); + int code; + /* + * 特征提取,注册人脸时extractType值为ExtractType.REGISTER,mask的值为MaskInfo.NOT_WORN + */ + synchronized (registerFaceEngine) { + code = registerFaceEngine.extractFaceFeature(nv21, width, height, FaceEngine.CP_PAF_NV21, faceInfo.getFaceInfoRgb(), + ExtractType.REGISTER, MaskInfo.NOT_WORN, faceFeature); + } + if (code != ErrorInfo.MOK) { + Log.e(TAG, "registerNv21: extractFaceFeature failed , code is " + code); + return null; + } else { + /* + * 1.保存注册结果(注册图、特征数据) + * 2.为了美观,扩大rect截取注册图 + */ + Rect cropRect = getBestRect(width, height, faceInfo.getFaceInfoRgb().getRect()); + if (cropRect == null) { + Log.e(TAG, "registerNv21: cropRect is null!"); + return null; + } + + cropRect.left &= ~3; + cropRect.top &= ~3; + cropRect.right &= ~3; + cropRect.bottom &= ~3; + + // 创建一个头像的Bitmap,存放旋转结果图 + Bitmap headBmp = getHeadImage(nv21, width, height, faceInfo.getFaceInfoRgb().getOrient(), cropRect, ArcSoftImageFormat.NV21); +// String imgPath = getImagePath(name); +// try { +// FileOutputStream fos = new FileOutputStream(imgPath); +// headBmp.compress(Bitmap.CompressFormat.JPEG, 100, fos); +// fos.close(); +// } catch (IOException e) { +// e.printStackTrace(); +// return null; +// } +// FaceEntity faceEntity = new FaceEntity(name, imgPath, faceFeature.getFeatureData()); +// long faceId = FaceDatabase.getInstance(context).faceDao().insert(faceEntity); +// faceEntity.setFaceId(faceId); +// registerFaceFeatureInfoFromDb(faceEntity, frEngine); + + + UserFaceInfo userFaceInfo = new UserFaceInfo(); + userFaceInfo.setFaceFeature(faceFeature); + userFaceInfo.setHeadBmp(headBmp); + userFaceInfo.setFrEngine(frEngine); + + return userFaceInfo; + } + + } + + /** + * 通过FaceEngine注册多个人脸数据 + * + * @param faceEngine 指定FaceEngine + * @param faceEntityList 人脸数据集 + */ + private void registerFaceFeatureInfoListFromDb(FaceEngine faceEngine, List faceEntityList) { + List faceFeatureInfoList = new ArrayList<>(); + for (FaceEntity faceEntity : faceEntityList) { + FaceFeatureInfo faceFeatureInfo = new FaceFeatureInfo((int) faceEntity.getFaceId(), faceEntity.getFeatureData()); + faceFeatureInfoList.add(faceFeatureInfo); + } + if (faceEngine != null) { + //首先清除FaceEngine中所有人脸数据,再添加新的人脸数据 + faceEngine.removeFaceFeature(-1); + int res = faceEngine.registerFaceFeature(faceFeatureInfoList); + Log.i(TAG, "registerFaceFeature:" + res); + } + } + + /** + * 通过FaceEngine注册单个人脸数据 + * + * @param faceEngine 指定FaceEngine + * @param faceEntity 指定人脸数据 + */ + public void registerFaceFeatureInfoFromDb(FaceEntity faceEntity, FaceEngine faceEngine) { + if (faceEntity != null && faceEngine != null) { + FaceFeatureInfo faceFeatureInfo = new FaceFeatureInfo((int) faceEntity.getFaceId(), faceEntity.getFeatureData()); + int res = faceEngine.registerFaceFeature(faceFeatureInfo); + Log.i(TAG, "registerFaceFeature:" + res); + } + } + + /** + * 获取存放注册照的文件夹路径 + * + * @return 存放注册照的文件夹路径 + */ + private String getImageDir() { +// return App.getContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES) +// + File.separator + "faceDB" + File.separator + "registerFaces"; + return App.getContext().getFilesDir() + + File.separator + "faceDB" + + File.separator + "registerFaces"; + } + + /** + * 根据用户名获取注册图保存路径 + * + * @param name 用户名 + * @return 图片保存地址 + */ + private String getImagePath(String name) { + if (imageRootPath == null) { + imageRootPath = getImageDir(); + File dir = new File(imageRootPath); + if (!dir.exists() && !dir.mkdirs()) { + return null; + } + } + return imageRootPath + File.separator + name + "_" + System.currentTimeMillis() + ".jpg"; + } + + /** + * 注册一个jpg数据 + * + * @param context + * @param jpeg + * @param name + * @return + */ + public FaceEntity registerJpeg(Context context, byte[] jpeg, String name) throws RegisterFailedException { + if (faceRegisterInfoList != null && faceRegisterInfoList.size() >= MAX_REGISTER_FACE_COUNT) { + Log.e(TAG, "registerJpeg: registered face count limited " + faceRegisterInfoList.size()); + // 已达注册上限,超过该值会影响识别率 + throw new RegisterFailedException("registered face count limited"); + } + Bitmap bitmap = ImageUtil.jpegToScaledBitmap(jpeg, ImageUtil.DEFAULT_MAX_WIDTH, ImageUtil.DEFAULT_MAX_HEIGHT); + bitmap = ArcSoftImageUtil.getAlignedBitmap(bitmap, true); + byte[] imageData = ArcSoftImageUtil.createImageData(bitmap.getWidth(), bitmap.getHeight(), ArcSoftImageFormat.BGR24); + int code = ArcSoftImageUtil.bitmapToImageData(bitmap, imageData, ArcSoftImageFormat.BGR24); + if (code != ArcSoftImageUtilError.CODE_SUCCESS) { + throw new RuntimeException("bitmapToImageData failed, code is " + code); + } + return registerBgr24(context, imageData, bitmap.getWidth(), bitmap.getHeight(), name); + } + + /** + * 用于注册照片人脸 + * + * @param context 上下文对象 + * @param bgr24 bgr24数据 + * @param width bgr24宽度 + * @param height bgr24高度 + * @param name 保存的名字,若为空则使用时间戳 + * @return 注册成功后的人脸信息 + */ + public FaceEntity registerBgr24(Context context, byte[] bgr24, int width, int height, String name) { + if (faceEngine == null || context == null || bgr24 == null || width % 4 != 0 || bgr24.length != width * height * 3) { + Log.e(TAG, "registerBgr24: invalid params"); + return null; + } + //人脸检测 + List faceInfoList = new ArrayList<>(); + int code; + synchronized (faceEngine) { + code = faceEngine.detectFaces(bgr24, width, height, FaceEngine.CP_PAF_BGR24, faceInfoList); + } + if (code == ErrorInfo.MOK && !faceInfoList.isEmpty()) { + code = faceEngine.process(bgr24, width, height, FaceEngine.CP_PAF_BGR24, faceInfoList, + FaceEngine.ASF_MASK_DETECT); + if (code == ErrorInfo.MOK) { + List maskInfoList = new ArrayList<>(); + faceEngine.getMask(maskInfoList); + if (!maskInfoList.isEmpty()) { + int isMask = maskInfoList.get(0).getMask(); + if (isMask == MaskInfo.WORN) { + /* + * 注册照要求不戴口罩 + */ + Log.e(TAG, "registerBgr24: maskInfo is worn"); + return null; + } + } + } + + FaceFeature faceFeature = new FaceFeature(); + /* + * 特征提取,注册人脸时参数extractType值为ExtractType.REGISTER,参数mask的值为MaskInfo.NOT_WORN + */ + synchronized (faceEngine) { + code = faceEngine.extractFaceFeature(bgr24, width, height, FaceEngine.CP_PAF_BGR24, faceInfoList.get(0), + ExtractType.REGISTER, MaskInfo.NOT_WORN, faceFeature); + } + String userName = name == null ? String.valueOf(System.currentTimeMillis()) : name; + + //保存注册结果(注册图、特征数据) + if (code == ErrorInfo.MOK) { + //为了美观,扩大rect截取注册图 + Rect cropRect = getBestRect(width, height, faceInfoList.get(0).getRect()); + if (cropRect == null) { + Log.e(TAG, "registerBgr24: cropRect is null"); + return null; + } + + cropRect.left &= ~3; + cropRect.top &= ~3; + cropRect.right &= ~3; + cropRect.bottom &= ~3; + + String imgPath = getImagePath(userName); + + // 创建一个头像的Bitmap,存放旋转结果图 + Bitmap headBmp = getHeadImage(bgr24, width, height, faceInfoList.get(0).getOrient(), cropRect, ArcSoftImageFormat.BGR24); + + try { + FileOutputStream fos = new FileOutputStream(imgPath); + headBmp.compress(Bitmap.CompressFormat.JPEG, 100, fos); + fos.close(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + + // 内存中的数据同步 + if (faceRegisterInfoList == null) { + faceRegisterInfoList = new ArrayList<>(); + } + FaceEntity faceEntity = new FaceEntity(name, imgPath, faceFeature.getFeatureData()); + long faceId = FaceDatabase.getInstance(context).faceDao().insert(faceEntity); + faceEntity.setFaceId(faceId); + faceRegisterInfoList.add(faceEntity); + return faceEntity; + } else { + Log.e(TAG, "registerBgr24: extract face feature failed, code is " + code); + return null; + } + } else { + Log.e(TAG, "registerBgr24: no face detected, code is " + code); + return null; + } + } + + /** + * 截取合适的头像并旋转,保存为注册头像 + * + * @param originImageData 原始的BGR24数据 + * @param width BGR24图像宽度 + * @param height BGR24图像高度 + * @param orient 人脸角度 + * @param cropRect 裁剪的位置 + * @param imageFormat 图像格式 + * @return 头像的图像数据 + */ + private Bitmap getHeadImage(byte[] originImageData, int width, int height, int orient, Rect cropRect, ArcSoftImageFormat imageFormat) { + byte[] headImageData = ArcSoftImageUtil.createImageData(cropRect.width(), cropRect.height(), imageFormat); + int cropCode = ArcSoftImageUtil.cropImage(originImageData, headImageData, width, height, cropRect, imageFormat); + if (cropCode != ArcSoftImageUtilError.CODE_SUCCESS) { + throw new RuntimeException("crop image failed, code is " + cropCode); + } + + //判断人脸旋转角度,若不为0度则旋转注册图 + byte[] rotateHeadImageData = null; + int cropImageWidth; + int cropImageHeight; + // 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(); + } + ArcSoftRotateDegree rotateDegree = null; + switch (orient) { + case FaceEngine.ASF_OC_90: + rotateDegree = ArcSoftRotateDegree.DEGREE_270; + break; + case FaceEngine.ASF_OC_180: + rotateDegree = ArcSoftRotateDegree.DEGREE_180; + break; + case FaceEngine.ASF_OC_270: + rotateDegree = ArcSoftRotateDegree.DEGREE_90; + break; + case FaceEngine.ASF_OC_0: + default: + rotateHeadImageData = headImageData; + break; + } + // 非0度的情况,旋转图像 + if (rotateDegree != null) { + rotateHeadImageData = new byte[headImageData.length]; + int rotateCode = ArcSoftImageUtil.rotateImage(headImageData, rotateHeadImageData, cropRect.width(), cropRect.height(), rotateDegree, imageFormat); + if (rotateCode != ArcSoftImageUtilError.CODE_SUCCESS) { + throw new RuntimeException("rotate image failed, code is : " + rotateCode + ", code description is : " + ErrorCodeUtil.imageUtilErrorCodeToFieldName(rotateCode)); + } + } + // 将创建一个Bitmap,并将图像数据存放到Bitmap中 + Bitmap headBmp = Bitmap.createBitmap(cropImageWidth, cropImageHeight, Bitmap.Config.RGB_565); + int imageDataToBitmapCode = ArcSoftImageUtil.imageDataToBitmap(rotateHeadImageData, headBmp, imageFormat); + if (imageDataToBitmapCode != ArcSoftImageUtilError.CODE_SUCCESS) { + throw new RuntimeException("failed to transform image data to bitmap, code is : " + imageDataToBitmapCode + + ", code description is : " + ErrorCodeUtil.imageUtilErrorCodeToFieldName(imageDataToBitmapCode)); + } + return headBmp; + } + + /** + * 在特征库中搜索 + * + * @param faceFeature 传入特征数据 + * @param faceEngine 指定FaceEngine + * @return 比对结果 + */ + public CompareResult searchFaceFeature(FaceFeature faceFeature, FaceEngine faceEngine) { + if (faceEngine == null || faceFeature == null) { + return null; + } + long start = System.currentTimeMillis(); + SearchResult searchResult; + try { + long searchStart = System.currentTimeMillis(); + searchResult = faceEngine.searchFaceFeature(faceFeature); + Log.i(TAG, "searchCost:" + (System.currentTimeMillis() - searchStart) + "ms"); + if (searchResult != null) { + FaceFeatureInfo faceFeatureInfo = searchResult.getFaceFeatureInfo(); + FaceEntity faceEntity = FaceDatabase.getInstance(App.getContext()).faceDao().queryByFaceId(faceFeatureInfo.getSearchId()); + if (faceEntity != null) { + return new CompareResult(faceEntity, searchResult.getMaxSimilar(), ErrorInfo.MOK, System.currentTimeMillis() - start); + } + } + } catch (IllegalArgumentException exception) { + Log.i(TAG, "searchFaceFeature exception:" + exception.getMessage()); + } + return null; + } + + /** + * 将图像中需要截取的Rect向外扩张一倍,若扩张一倍会溢出,则扩张到边界,若Rect已溢出,则收缩到边界 + * + * @param width 图像宽度 + * @param height 图像高度 + * @param srcRect 原Rect + * @return 调整后的Rect + */ + private static Rect getBestRect(int width, int height, Rect srcRect) { + if (srcRect == null) { + return null; + } + Rect rect = new Rect(srcRect); + + // 原rect边界已溢出宽高的情况 + int 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边界未溢出宽高的情况 + int 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; + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/faceserver/RegisterFailedException.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/faceserver/RegisterFailedException.java new file mode 100644 index 0000000..bf115eb --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/faceserver/RegisterFailedException.java @@ -0,0 +1,7 @@ +package com.sw.plate.utils.arcface.faceserver; + +public class RegisterFailedException extends Exception { + public RegisterFailedException(String message) { + super(message); + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/model/UserFaceInfo.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/model/UserFaceInfo.java new file mode 100644 index 0000000..94eb7fe --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/model/UserFaceInfo.java @@ -0,0 +1,37 @@ +package com.sw.plate.utils.arcface.model; + +import android.graphics.Bitmap; + +import com.arcsoft.face.FaceEngine; +import com.arcsoft.face.FaceFeature; + +public class UserFaceInfo { + private FaceEngine frEngine; + private Bitmap headBmp; + private FaceFeature faceFeature; + + public FaceEngine getFrEngine() { + return frEngine; + } + + public void setFrEngine(FaceEngine frEngine) { + this.frEngine = frEngine; + } + + public Bitmap getHeadBmp() { + return headBmp; + } + + public void setHeadBmp(Bitmap headBmp) { + this.headBmp = headBmp; + } + + public FaceFeature getFaceFeature() { + return faceFeature; + } + + public void setFaceFeature(FaceFeature faceFeature) { + this.faceFeature = faceFeature; + } + +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/view/RecognizeAreaView.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/view/RecognizeAreaView.java new file mode 100644 index 0000000..6d707e3 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/view/RecognizeAreaView.java @@ -0,0 +1,189 @@ +package com.sw.plate.utils.arcface.view; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.os.Build; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; + +import com.sw.plate.R; +import com.sw.plate.utils.arcface.FaceRectView; + + +/** + * 控制可识别区域的控件,中间的镂空区域为可识别区域。 + *

+ * 结合{@link FaceRectView}和{@link com.arcsoft.arcfacedemo.util.FaceRectTransformer}使用,可判断人脸是否显示在镂空区域 + *

+ * 注意:需要保证人脸框绘制正确,识别区域的控制才有效。 + *

+ * 实际使用中建议不要实现onTouch + */ +public class RecognizeAreaView extends View implements View.OnTouchListener { + /** + * 限制的识别区域 + */ + private RectF limitArea; + + /** + * 不可识别区域的颜色 + */ + private int shadowColor; + + /** + * 触摸点到当前识别区域的4个顶点距离的平方 + * 0:左上角 + * 1:右上角 + * 2:左下角 + * 3:右下角 + */ + private double[] distanceSquares = new double[4]; + + /** + * 识别区域发生变更的回调 + */ + public interface OnRecognizeAreaChangedListener { + /** + * 当识别区域发生变更时执行 + * + * @param recognizeArea 新的识别区域(相对于View,而非图像数据) + */ + void onRecognizeAreaChanged(Rect recognizeArea); + } + + OnRecognizeAreaChangedListener onRecognizeAreaChangedListener; + + /** + * 设置识别区域发生变更的回调 + * + * @param onRecognizeAreaChangedListener 识别区域发生变更的回调 + */ + public void setOnRecognizeAreaChangedListener(OnRecognizeAreaChangedListener onRecognizeAreaChangedListener) { + this.onRecognizeAreaChangedListener = onRecognizeAreaChangedListener; + } + + public RecognizeAreaView(Context context) { + this(context, null); + } + + public RecognizeAreaView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + shadowColor = ContextCompat.getColor(context, R.color.color_bg_notification); + setOnTouchListener(this); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + limitArea = new RectF(0, 0, width, height); + if (onRecognizeAreaChangedListener != null) { + onRecognizeAreaChangedListener.onRecognizeAreaChanged( + new Rect(((int) limitArea.left), ((int) limitArea.top), + ((int) limitArea.right), ((int) limitArea.bottom)) + ); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (limitArea == null) { + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvas.clipOutRect(limitArea); + } else { + canvas.clipRect(limitArea, Region.Op.DIFFERENCE); + } + canvas.drawColor(shadowColor); + } + + /** + * 根据最近的触摸点,刷新识别区域 + * + * @param x 触摸点的横坐标 + * @param y 触摸点的纵坐标 + */ + private void updateRecognizeArea(float x, float y) { + /* + 0:左上角 + 1:右上角 + 2:左下角 + 3:右下角 + */ + distanceSquares[0] = getDistanceSquare(x, y, limitArea.left, limitArea.top); + distanceSquares[1] = getDistanceSquare(x, y, limitArea.right, limitArea.top); + distanceSquares[2] = getDistanceSquare(x, y, limitArea.left, limitArea.bottom); + distanceSquares[3] = getDistanceSquare(x, y, limitArea.right, limitArea.bottom); + + int closestIndex = 0; + double closestDistance = distanceSquares[0]; + for (int i = 1; i < distanceSquares.length; i++) { + double distance = distanceSquares[i]; + if (closestDistance > distance) { + closestDistance = distance; + closestIndex = i; + } + } + switch (closestIndex) { + case 0: + limitArea.left = x; + limitArea.top = y; + break; + case 1: + limitArea.right = x; + limitArea.top = y; + break; + case 2: + limitArea.left = x; + limitArea.bottom = y; + break; + case 3: + limitArea.right = x; + limitArea.bottom = y; + break; + default: + break; + } + } + + /** + * 获取两点距离的平方(由于只是为了大小比较,所以没必要开根号,减少运算) + * + * @param x1 第一个点的横坐标 + * @param y1 第一个点的纵坐标 + * @param x2 第二个点的横坐标 + * @param y2 第二个点的纵坐标 + * @return 距离的平方 + */ + private double getDistanceSquare(float x1, float y1, float x2, float y2) { + float deltaHorizontal = x1 - x2; + float deltaVertical = y1 - y2; + return deltaHorizontal * deltaHorizontal + deltaVertical * deltaVertical; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + int pointerCount = event.getPointerCount(); + for (int i = 0; i < pointerCount; i++) { + updateRecognizeArea(event.getX(i), event.getY(i)); + } + if (onRecognizeAreaChangedListener != null) { + onRecognizeAreaChangedListener.onRecognizeAreaChanged( + new Rect(((int) limitArea.left), ((int) limitArea.top), + ((int) limitArea.right), ((int) limitArea.bottom)) + ); + } + invalidate(); + return true; + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/viewmodel/ActiveViewModel.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/viewmodel/ActiveViewModel.java new file mode 100644 index 0000000..04c3082 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/viewmodel/ActiveViewModel.java @@ -0,0 +1,71 @@ +package com.sw.plate.utils.arcface.viewmodel; + +import android.content.Context; +import android.os.Environment; + +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.arcsoft.face.FaceEngine; +import com.sw.plate.AppConst; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; + +public class ActiveViewModel extends ViewModel { + private MutableLiveData activeResult = new MutableLiveData<>(); + + public void activeOnline(Context context, String activeKey, String appId, String sdkKey) { + activeResult.postValue(FaceEngine.activeOnline(context, activeKey, appId, sdkKey)); + } + + public void activeOffline(Context context, String path) { + activeResult.postValue(FaceEngine.activeOffline(context, path)); + } + + private static final int ACTIVE_KEY_EFFECTIVE_LENGTH = 16; + + public String formatActiveKey(String activeKey) { + String rawActiveKey = activeKey.replace("-", "").toUpperCase(); + StringBuilder newActiveKey = new StringBuilder(); + if (rawActiveKey.length() == ACTIVE_KEY_EFFECTIVE_LENGTH) { + for (int i = 0; i < 4; i++) { + newActiveKey.append(rawActiveKey.substring(i * 4, i * 4 + 4)).append("-"); + } + newActiveKey.deleteCharAt(newActiveKey.length() - 1); + return newActiveKey.toString(); + } else { + return activeKey; + } + + } + + public MutableLiveData getActiveResult() { + return activeResult; + } + + + public Properties loadProperties() { + Properties properties = new Properties(); + FileInputStream fis = null; + File configFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + AppConst.ACTIVE_CONFIG_FILE_NAME); + try { + fis = new FileInputStream(configFile); + properties.load(fis); + return properties; + } catch (IOException e) { + e.printStackTrace(); + return null; + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } +} diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/viewmodel/RecognizeViewModel.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/viewmodel/RecognizeViewModel.java new file mode 100644 index 0000000..7fe5798 --- /dev/null +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/viewmodel/RecognizeViewModel.java @@ -0,0 +1,668 @@ +package com.sw.plate.utils.arcface.viewmodel; + +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.hardware.Camera; +import android.util.Log; +import android.widget.Toast; + +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.arcsoft.face.AgeInfo; +import com.arcsoft.face.ErrorInfo; +import com.arcsoft.face.FaceAttributeParam; +import com.arcsoft.face.FaceEngine; +import com.arcsoft.face.FaceInfo; +import com.arcsoft.face.GenderInfo; +import com.arcsoft.face.LivenessInfo; +import com.arcsoft.face.LivenessParam; +import com.arcsoft.face.MaskInfo; +import com.arcsoft.face.enums.DetectFaceOrientPriority; +import com.arcsoft.face.enums.DetectMode; +import com.sw.plate.App; +import com.sw.plate.R; +import com.sw.plate.utils.arcface.ConfigUtil; +import com.sw.plate.utils.arcface.FaceRectTransformer; +import com.sw.plate.utils.arcface.FaceRectView; +import com.sw.plate.utils.arcface.PreviewConfig; +import com.sw.plate.utils.arcface.callback.OnRegisterFinishedCallback; +import com.sw.plate.utils.arcface.face.FaceHelper; +import com.sw.plate.utils.arcface.face.RecognizeCallback; +import com.sw.plate.utils.arcface.face.constants.LivenessType; +import com.sw.plate.utils.arcface.face.constants.RecognizeColor; +import com.sw.plate.utils.arcface.face.constants.RequestFeatureStatus; +import com.sw.plate.utils.arcface.face.model.CompareResult; +import com.sw.plate.utils.arcface.face.model.FacePreviewInfo; +import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration; +import com.sw.plate.utils.arcface.facedb.entity.FaceEntity; +import com.sw.plate.utils.arcface.faceserver.FaceServer; +import com.sw.plate.utils.arcface.model.UserFaceInfo; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import io.reactivex.Observable; +import io.reactivex.ObservableOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.observers.DisposableObserver; +import io.reactivex.schedulers.Schedulers; + +public class RecognizeViewModel extends ViewModel implements RecognizeCallback { + /** + * 人脸识别过程中数据的更新类型 + */ + public enum EventType { + /** + * 人脸插入 + */ + INSERTED, + /** + * 人脸移除 + */ + REMOVED + } + + public static class FaceItemEvent { + private int index; + private EventType eventType; + + public FaceItemEvent(int index, EventType eventType) { + this.index = index; + this.eventType = eventType; + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public EventType getEventType() { + return eventType; + } + + public void setEventType(EventType eventType) { + this.eventType = eventType; + } + } + + private static final String TAG = "RecognizeViewModel"; + + + private OnRegisterFinishedCallback onRegisterFinishedCallback; + + /** + * 注册人脸状态码,准备注册 + */ + public static final int REGISTER_STATUS_READY = 0; + /** + * 注册人脸状态码,注册中 + */ + public static final int REGISTER_STATUS_PROCESSING = 1; + /** + * 注册人脸状态码,注册结束(无论成功失败) + */ + public static final int REGISTER_STATUS_DONE = 2; + + /** + * 人脸识别的状态,预设值为:已结束 + */ + private int registerStatus = REGISTER_STATUS_DONE; + + private static final int MAX_DETECT_NUM = 10; + /** + * 相机预览的分辨率 + */ + private Camera.Size previewSize; + /** + * 用于头像RecyclerView显示的信息 + */ + private MutableLiveData> compareResultList; + + private MutableLiveData faceItemEventMutableLiveData = new MutableLiveData<>(); + + /** + * 各个引擎初始化的错误码 + */ + private MutableLiveData ftInitCode = new MutableLiveData<>(); + private MutableLiveData frInitCode = new MutableLiveData<>(); + private MutableLiveData flInitCode = new MutableLiveData<>(); + + /** + * 人脸操作辅助类,推帧即可,内部会进行特征提取、识别 + */ + private FaceHelper faceHelper; + /** + * VIDEO模式人脸检测引擎,用于预览帧人脸追踪及图像质量检测 + */ + private FaceEngine ftEngine; + /** + * 用于特征提取的引擎 + */ + private FaceEngine frEngine; + /** + * IMAGE模式活体检测引擎,用于预览帧人脸活体检测 + */ + private FaceEngine flEngine; + + private PreviewConfig previewConfig; + + private MutableLiveData recognizeConfiguration = new MutableLiveData<>(); + + private MutableLiveData recognizeNotice = new MutableLiveData<>(); + + private MutableLiveData drawRectInfoText = new MutableLiveData<>(); + + private MutableLiveData recognizeUserId = new MutableLiveData<>(); + + /** + * 检测ir活体前,是否需要更新faceData + */ + private boolean needUpdateFaceData; + /** + * 当前活体检测的检测类型 + */ + private LivenessType livenessType; + + /** + * IR活体数据 + */ + private byte[] irNV21 = null; + + /** + * 人脸库数据加载完成 + */ + private boolean loadFaceList; + + private Disposable registerNv21Disposable; + + public void refreshIrPreviewData(byte[] irPreviewData) { + irNV21 = irPreviewData; + } + + /** + * 设置当前活体检测的检测类型 + * + * @param liveType 活体检测的检测类型 + */ + public void setLiveType(LivenessType liveType) { + this.livenessType = liveType; + } + + public void setRgbFaceRectTransformer(FaceRectTransformer rgbFaceRectTransformer) { + faceHelper.setRgbFaceRectTransformer(rgbFaceRectTransformer); + } + + public void setIrFaceRectTransformer(FaceRectTransformer irFaceRectTransformer) { + faceHelper.setIrFaceRectTransformer(irFaceRectTransformer); + } + + + /** + * 注册实时NV21数据 + * + * @param nv21 实时相机预览的NV21数据 + * @param facePreviewInfo 人脸信息 + */ + private void registerFace(final byte[] nv21, FacePreviewInfo facePreviewInfo) { + updateRegisterStatus(REGISTER_STATUS_PROCESSING); + registerNv21Disposable = Observable.create((ObservableOnSubscribe) emitter -> { + FaceEngine registerEngine = new FaceEngine(); + int res = registerEngine.init(App.getContext(), DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_0_ONLY, + 1, FaceEngine.ASF_FACE_RECOGNITION); + if (res == ErrorInfo.MOK) { +// boolean success = FaceServer.getInstance().registerNv21(App.getContext(), nv21.clone(), previewSize.width, +// previewSize.height, facePreviewInfo, "registered_" + faceHelper.getTrackedFaceCount(), frEngine, registerEngine); + + UserFaceInfo userFaceInfo = FaceServer.getInstance().getUserInfo(App.getContext(), nv21.clone(), previewSize.width, + previewSize.height, facePreviewInfo, "registered_" + faceHelper.getTrackedFaceCount(), frEngine, registerEngine); + registerEngine.unInit(); + emitter.onNext(userFaceInfo); + } else { + emitter.onNext(null); + } + emitter.onComplete(); + }) + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeWith(new DisposableObserver() { + + @Override + public void onNext(UserFaceInfo success) { + if (onRegisterFinishedCallback != null) { + onRegisterFinishedCallback.onRegisterFinished(facePreviewInfo, success); + } + + updateRegisterStatus(REGISTER_STATUS_DONE); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + if (onRegisterFinishedCallback != null) { + onRegisterFinishedCallback.onRegisterFinished(facePreviewInfo, null); + } + updateRegisterStatus(REGISTER_STATUS_DONE); + } + + @Override + public void onComplete() { + } + }); + + } + + public MutableLiveData> getCompareResultList() { + if (compareResultList == null) { + compareResultList = new MutableLiveData<>(); + compareResultList.setValue(new ArrayList<>()); + } + return compareResultList; + } + + /** + * 初始化引擎 + */ + public void init(PreviewConfig previewConfig1) { + Context context = App.getContext(); + if (previewConfig1 != null) { + previewConfig = previewConfig1; + } else { + boolean switchCamera = ConfigUtil.isSwitchCamera(context); + previewConfig = new PreviewConfig( + switchCamera ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK, + switchCamera ? Camera.CameraInfo.CAMERA_FACING_BACK : Camera.CameraInfo.CAMERA_FACING_FRONT, + Integer.parseInt(ConfigUtil.getRgbCameraAdditionalRotation(context)), + Integer.parseInt(ConfigUtil.getIrCameraAdditionalRotation(context)) + ); + } + // 填入在设置界面设置好的配置信息 + boolean enableLive = !ConfigUtil.getLivenessDetectType(context).equals(context.getString(R.string.value_liveness_type_disable)); +// enableLive = false; + boolean enableFaceQualityDetect = ConfigUtil.isEnableImageQualityDetect(context); + boolean enableFaceMoveLimit = ConfigUtil.isEnableFaceMoveLimit(context); + boolean enableFaceSizeLimit = ConfigUtil.isEnableFaceSizeLimit(context); + RecognizeConfiguration configuration = new RecognizeConfiguration.Builder() + .enableFaceMoveLimit(enableFaceMoveLimit) + .enableFaceSizeLimit(enableFaceSizeLimit) + .faceSizeLimit(ConfigUtil.getFaceSizeLimit(context)) + .faceMoveLimit(ConfigUtil.getFaceMoveLimit(context)) + .enableLiveness(enableLive) + .enableImageQuality(enableFaceQualityDetect) + .maxDetectFaces(ConfigUtil.getRecognizeMaxDetectFaceNum(context)) + .keepMaxFace(ConfigUtil.isKeepMaxFace(context)) + .similarThreshold(ConfigUtil.getRecognizeThreshold(context)) + .imageQualityNoMaskRecognizeThreshold(ConfigUtil.getImageQualityNoMaskRecognizeThreshold(context)) + .imageQualityMaskRecognizeThreshold(ConfigUtil.getImageQualityMaskRecognizeThreshold(context)) + .livenessParam(new LivenessParam(ConfigUtil.getRgbLivenessThreshold(context), ConfigUtil.getIrLivenessThreshold(context), + ConfigUtil.getLivenessFqThreshold(context))) + .build(); + int cameraOffsetX = ConfigUtil.getDualCameraHorizontalOffset(context); + int cameraOffsetY = ConfigUtil.getDualCameraVerticalOffset(context); + needUpdateFaceData = (livenessType == LivenessType.IR && (cameraOffsetX != 0 || cameraOffsetY != 0)); + + ftEngine = new FaceEngine(); + int ftEngineMask = FaceEngine.ASF_FACE_DETECT | FaceEngine.ASF_MASK_DETECT; + ftInitCode.postValue(ftEngine.init(context, DetectMode.ASF_DETECT_MODE_VIDEO, ConfigUtil.getFtOrient(context), + ConfigUtil.getRecognizeMaxDetectFaceNum(context), ftEngineMask)); + FaceAttributeParam attributeParam = new FaceAttributeParam( + ConfigUtil.getRecognizeEyeOpenThreshold(context), ConfigUtil.getRecognizeMouthCloseThreshold(context), + ConfigUtil.getRecognizeWearGlassesThreshold(context)); + ftEngine.setFaceAttributeParam(attributeParam); + + frEngine = new FaceEngine(); + int frEngineMask = FaceEngine.ASF_FACE_RECOGNITION; + if (enableFaceQualityDetect) { + frEngineMask |= FaceEngine.ASF_IMAGEQUALITY; + } + frInitCode.postValue(frEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_0_ONLY, + 10, frEngineMask)); + FaceServer.getInstance().initFaceList(context, frEngine, faceCount -> loadFaceList = true, true); + + //启用活体检测时,才初始化活体引擎 + if (enableLive) { + flEngine = new FaceEngine(); + int flEngineMask = (livenessType == LivenessType.RGB ? FaceEngine.ASF_LIVENESS : (FaceEngine.ASF_IR_LIVENESS | FaceEngine.ASF_FACE_DETECT)); + if (needUpdateFaceData) { + flEngineMask |= FaceEngine.ASF_UPDATE_FACEDATA; + } + flInitCode.postValue(flEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE, + DetectFaceOrientPriority.ASF_OP_ALL_OUT, 10, flEngineMask)); + LivenessParam livenessParam = new LivenessParam(ConfigUtil.getRgbLivenessThreshold(context), ConfigUtil.getIrLivenessThreshold(context), ConfigUtil.getLivenessFqThreshold(context)); + flEngine.setLivenessParam(livenessParam); + } + + recognizeConfiguration.setValue(configuration); + } + + public void addFace(FaceEntity faceEntity) { + if (frEngine != null) + FaceServer.getInstance().registerFaceFeatureInfoFromDb(faceEntity, frEngine); + } + + public void refreshFaceList() { + FaceServer.getInstance().initFaceList(App.getContext(), frEngine, faceCount -> loadFaceList = true, true); + } + + /** + * 销毁引擎,faceHelper中可能会有特征提取耗时操作仍在执行,加锁防止crash + */ + private void unInit() { + if (ftEngine != null) { + synchronized (ftEngine) { + int ftUnInitCode = ftEngine.unInit(); + Log.i(TAG, "unInitEngine: " + ftUnInitCode); + } + } + if (frEngine != null) { + synchronized (frEngine) { + int frUnInitCode = frEngine.unInit(); + Log.i(TAG, "unInitEngine: " + frUnInitCode); + } + } + if (flEngine != null) { + synchronized (flEngine) { + int flUnInitCode = flEngine.unInit(); + Log.i(TAG, "unInitEngine: " + flUnInitCode); + } + } + } + + /** + * 删除已经离开的人脸 + * + * @param facePreviewInfoList 人脸和trackId列表 + */ + public void clearLeftFace(List facePreviewInfoList) { + List compareResults = compareResultList.getValue(); + if (compareResults != null) { + for (int i = compareResults.size() - 1; i >= 0; i--) { + boolean contains = false; + for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) { + if (facePreviewInfo.getTrackId() == compareResults.get(i).getTrackId()) { + contains = true; + break; + } + } + if (!contains) { + compareResults.remove(i); + getFaceItemEventMutableLiveData().postValue(new FaceItemEvent(i, EventType.REMOVED)); + } + } + } + } + + /** + * 释放操作 + */ + public void destroy() { + unInit(); + if (faceHelper != null) { + ConfigUtil.setTrackedFaceCount(App.getContext(), faceHelper.getTrackedFaceCount()); + faceHelper.release(); + faceHelper = null; + } + FaceServer.getInstance().release(); + if (registerNv21Disposable != null) { + registerNv21Disposable.dispose(); + registerNv21Disposable = null; + } + } + + /** + * 当相机打开时由activity调用,进行一些初始化操作 + * + * @param camera 相机实例 + */ + public void onRgbCameraOpened(Camera camera) { + Camera.Size lastPreviewSize = previewSize; + previewSize = camera.getParameters().getPreviewSize(); + // 切换相机的时候可能会导致预览尺寸发生变化 + initFaceHelper(lastPreviewSize); + } + + /** + * 当相机打开时由activity调用,进行一些初始化操作 + * + * @param camera 相机实例 + */ + public void onIrCameraOpened(Camera camera) { + Camera.Size lastPreviewSize = previewSize; + previewSize = camera.getParameters().getPreviewSize(); + // 切换相机的时候可能会导致预览尺寸发生变化 + initFaceHelper(lastPreviewSize); + } + + private void initFaceHelper(Camera.Size lastPreviewSize) { + if (faceHelper == null || lastPreviewSize == null || + lastPreviewSize.width != previewSize.width || lastPreviewSize.height != previewSize.height) { + Integer trackedFaceCount = null; + // 记录切换时的人脸序号 + if (faceHelper != null) { + trackedFaceCount = faceHelper.getTrackedFaceCount(); + faceHelper.release(); + } + Context context = App.getContext(); + int horizontalOffset = ConfigUtil.getDualCameraHorizontalOffset(context); + int verticalOffset = ConfigUtil.getDualCameraVerticalOffset(context); + int maxDetectFaceNum = ConfigUtil.getRecognizeMaxDetectFaceNum(context); + faceHelper = new FaceHelper.Builder() + .ftEngine(ftEngine) + .frEngine(frEngine) + .flEngine(flEngine) + .needUpdateFaceData(needUpdateFaceData) + .frQueueSize(maxDetectFaceNum) + .flQueueSize(maxDetectFaceNum) + .previewSize(previewSize) + .recognizeCallback(this) + .recognizeConfiguration(recognizeConfiguration.getValue()) + .trackedFaceCount(trackedFaceCount == null ? ConfigUtil.getTrackedFaceCount(context) : trackedFaceCount) + .dualCameraFaceInfoTransformer(faceInfo -> { + FaceInfo irFaceInfo = new FaceInfo(faceInfo); + irFaceInfo.getRect().offset(horizontalOffset, verticalOffset); + return irFaceInfo; + }) + .build(); + } + } + + @Override + public void onRecognized(CompareResult compareResult, Integer live, boolean similarPass) { + Disposable disposable = Observable.just(true).observeOn(AndroidSchedulers.mainThread()).subscribe(aBoolean -> { + if (similarPass) { + if (recognizeUserId != null) { + recognizeUserId.postValue(compareResult); + } + boolean isAdded = false; + List compareResults = compareResultList.getValue(); + if (compareResults != null && !compareResults.isEmpty()) { + for (CompareResult compareResult1 : compareResults) { + if (compareResult1.getTrackId() == compareResult.getTrackId()) { + isAdded = true; + break; + } + } + } + if (!isAdded) { + //对于多人脸搜索,假如最大显示数量为 MAX_DETECT_NUM 且有新的人脸进入,则以队列的形式移除 + if (compareResults != null && compareResults.size() >= MAX_DETECT_NUM) { + compareResults.remove(0); + getFaceItemEventMutableLiveData().postValue(new FaceItemEvent(0, EventType.REMOVED)); + } + if (compareResults != null) { + compareResults.add(compareResult); + getFaceItemEventMutableLiveData().postValue(new FaceItemEvent(compareResults.size() - 1, EventType.INSERTED)); + } + } + } + }); + } + + @Override + public void onNoticeChanged(String notice) { + if (recognizeNotice != null) { + recognizeNotice.postValue(notice); + } + } + + public void setDrawRectInfoTextValue(boolean openDrawRect) { + String stringDrawText = openDrawRect ? "关闭绘制" : "开启绘制"; + if (drawRectInfoText != null) { + drawRectInfoText.postValue(stringDrawText); + } + } + + /** + * 设置实时注册的结果回调 + * + * @param onRegisterFinishedCallback 实时注册的结果回调 + */ + public void setOnRegisterFinishedCallback(OnRegisterFinishedCallback onRegisterFinishedCallback) { + this.onRegisterFinishedCallback = onRegisterFinishedCallback; + } + + public MutableLiveData getFtInitCode() { + return ftInitCode; + } + + public MutableLiveData getFrInitCode() { + return frInitCode; + } + + public MutableLiveData getFlInitCode() { + return flInitCode; + } + + public MutableLiveData getRecognizeNotice() { + return recognizeNotice; + } + + public MutableLiveData getRecognizeUserId() { + return recognizeUserId; + } + + public MutableLiveData getDrawRectInfoText() { + return drawRectInfoText; + } + + public MutableLiveData getFaceItemEventMutableLiveData() { + return faceItemEventMutableLiveData; + } + + /** + * 准备注册,将注册的状态值修改为待注册 + */ + public void prepareRegister() { + if (registerStatus == REGISTER_STATUS_DONE) { + updateRegisterStatus(REGISTER_STATUS_READY); + } + } + + public void updateRegisterStatus(int status) { + registerStatus = status; + } + + /** + * 根据预览信息生成绘制信息 + * + * @param facePreviewInfoList 预览信息 + * @return 绘制信息 + */ + public List getDrawInfo(List facePreviewInfoList, LivenessType livenessType, boolean drawRectInfo) { + List drawInfoList = new ArrayList<>(); + for (int i = 0; i < facePreviewInfoList.size(); i++) { + int trackId = facePreviewInfoList.get(i).getTrackId(); + String name = faceHelper.getName(trackId); + Integer liveness = faceHelper.getLiveness(trackId); + Integer recognizeStatus = faceHelper.getRecognizeStatus(trackId); + + // 根据识别结果和活体结果设置颜色 + int color = RecognizeColor.COLOR_UNKNOWN; + if (recognizeStatus != null) { + if (recognizeStatus == RequestFeatureStatus.FAILED) { + color = RecognizeColor.COLOR_FAILED; + }else if (recognizeStatus == RequestFeatureStatus.SUCCEED) { + color = RecognizeColor.COLOR_SUCCESS; + } else if (recognizeStatus == RequestFeatureStatus.TO_RETRY) { + color = RecognizeColor.COLOR_UNKNOWN; + //需要重试 +// FaceEntity faceEntity = new FaceEntity("2",null, null); +// CompareResult result = new CompareResult(faceEntity, 0.0f); +// recognizeUserId.postValue(result); + } + } + if (liveness != null && liveness == LivenessInfo.NOT_ALIVE) { + color = RecognizeColor.COLOR_FAILED; + } + + drawInfoList.add(new FaceRectView.DrawInfo( + livenessType == LivenessType.RGB ? facePreviewInfoList.get(i).getRgbTransformedRect() : facePreviewInfoList.get(i).getIrTransformedRect(), + GenderInfo.UNKNOWN, AgeInfo.UNKNOWN_AGE, liveness == null ? LivenessInfo.UNKNOWN : liveness, color, + name == null ? "" : name, facePreviewInfoList.get(i).getFaceInfoRgb().getIsWithinBoundary(), + facePreviewInfoList.get(i).getForeRect(), facePreviewInfoList.get(i).getFaceInfoRgb().getFaceAttributeInfo(), drawRectInfo, + livenessType == LivenessType.RGB)); + } + return drawInfoList; + } + + + /** + * 传入可见光相机预览数据 + * + * @param nv21 可见光相机预览数据 + * @param doRecognize 是否进行识别 + * @return 当前帧的检测结果信息 + */ + public List onPreviewFrame(byte[] nv21, boolean doRecognize) { + if (faceHelper != null) { + if (!loadFaceList) { + return null; + } + if (livenessType == LivenessType.IR && irNV21 == null) { + return null; + } + List facePreviewInfoList = faceHelper.onPreviewFrame(nv21, irNV21, doRecognize); + if (registerStatus == REGISTER_STATUS_READY && !facePreviewInfoList.isEmpty()) { + FacePreviewInfo facePreviewInfo = facePreviewInfoList.get(0); + if (facePreviewInfo.getMask() != MaskInfo.WORN) { + registerFace(nv21, facePreviewInfoList.get(0)); + } else { + Toast.makeText(App.getContext(), "注册照要求不戴口罩", Toast.LENGTH_SHORT).show(); + updateRegisterStatus(REGISTER_STATUS_DONE); + } + } + return facePreviewInfoList; + } + return null; + } + + /** + * 设置可识别区域(相对于View) + * + * @param recognizeArea 可识别区域 + */ + public void setRecognizeArea(Rect recognizeArea) { + if (faceHelper != null) { + faceHelper.setRecognizeArea(recognizeArea); + } + } + + public MutableLiveData getRecognizeConfiguration() { + return recognizeConfiguration; + } + + public PreviewConfig getPreviewConfig() { + return previewConfig; + } + + public Point loadPreviewSize() { + String[] size = ConfigUtil.getPreviewSize(App.getContext()).split("x"); + return new Point(Integer.parseInt(size[0]), Integer.parseInt(size[1])); + } +} diff --git a/lib_face/src/main/res/layout/toast_bg.xml b/lib_face/src/main/res/layout/toast_bg.xml new file mode 100644 index 0000000..8c87595 --- /dev/null +++ b/lib_face/src/main/res/layout/toast_bg.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/lib_face/src/main/res/values/colors.xml b/lib_face/src/main/res/values/colors.xml new file mode 100644 index 0000000..706e518 --- /dev/null +++ b/lib_face/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #80000000 + #000000 + \ No newline at end of file diff --git a/lib_face/src/main/res/values/strings.xml b/lib_face/src/main/res/values/strings.xml new file mode 100644 index 0000000..47286d5 --- /dev/null +++ b/lib_face/src/main/res/values/strings.xml @@ -0,0 +1,49 @@ + + + + rgb_liveness + ir_liveness + disable_liveness + + track_face_count + choose_detect_degree + max_detect_num + limit_recognize_area + scale_value + dual_camera_offset_horizontal + dual_camera_offset_vertical + recognize_threshold + shelter_threshold + eye_open_threshold + mouth_close_threshold + wear_glasses_threshold + recognize_face_size_limit + recognize_move_pixel_limit + rgb_liveness_threshold + ir_liveness_threshold + liveness_fq_threshold + rgb_liveness_face_size_threshold + ir_liveness_face_size_threshold + dual_camera_preview_size + app_id + sdk_key + active_key + enable_image_quality_detect + enable_face_size_limit + enable_face_move_limit + image_quality_no_mask_recognize_threshold + image_quality_no_mask_register_threshold + image_quality_mask_recognize_threshold + + switch_camera + draw_rgb_rect_horizontal_mirror + draw_rgb_rect_vertical_mirror + draw_ir_rect_horizontal_mirror + draw_ir_rect_vertical_mirror + rgb_preview_horizontal_mirror + ir_preview_horizontal_mirror + + liveness_detect_type + rgb_camera_rotation + ir_camera_rotation + \ No newline at end of file diff --git a/lib_face/src/test/java/com/sw/plate/ExampleUnitTest.java b/lib_face/src/test/java/com/sw/plate/ExampleUnitTest.java new file mode 100644 index 0000000..44a803e --- /dev/null +++ b/lib_face/src/test/java/com/sw/plate/ExampleUnitTest.java @@ -0,0 +1,19 @@ +package com.sw.plate; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + int i1 = (int) (0.5f * 280); + System.err.println(""); + } +} \ No newline at end of file diff --git a/local.properties b/local.properties new file mode 100644 index 0000000..62c4a0c --- /dev/null +++ b/local.properties @@ -0,0 +1,10 @@ +## This file is automatically generated by Android Studio. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file should *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +sdk.dir=D\:\\Android\\sdk \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..162e0bf --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,36 @@ +@file:Suppress("UnstableApiUsage") + +pluginManagement { + repositories { + maven { url = uri("https://maven.aliyun.com/repository/public/") } + maven { url = uri("https://maven.aliyun.com/repository/central") } + maven { url = uri("https://maven.aliyun.com/repository/google") } + maven { url = uri("https://maven.aliyun.com/repository/jcenter") } + maven { url = uri("https://jitpack.io") } + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + maven { url = uri("https://maven.aliyun.com/repository/public/") } + maven { url = uri("https://maven.aliyun.com/repository/central") } + maven { url = uri("https://maven.aliyun.com/repository/google") } + maven { url = uri("https://maven.aliyun.com/repository/jcenter") } + maven { url = uri("https://jitpack.io") } + google() + mavenCentral() + } +} + +rootProject.name = "FaceCollect" +include(":app") +include(":lib_face")