init
This commit is contained in:
+27
-1
@@ -1,6 +1,9 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
|
id("com.google.devtools.ksp")
|
||||||
|
id("com.google.dagger.hilt.android")
|
||||||
|
id("kotlin-parcelize")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -9,7 +12,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.sw.dualscreen"
|
applicationId = "com.sw.dualscreen"
|
||||||
minSdk = 24
|
minSdk = 29
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
@@ -49,6 +52,8 @@ dependencies {
|
|||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|
||||||
|
implementation(project(":lib_face"))
|
||||||
|
|
||||||
// camerax
|
// camerax
|
||||||
implementation(libs.androidx.camera.core)
|
implementation(libs.androidx.camera.core)
|
||||||
implementation(libs.androidx.camera.camera2)
|
implementation(libs.androidx.camera.camera2)
|
||||||
@@ -58,4 +63,25 @@ dependencies {
|
|||||||
|
|
||||||
// 权限申请
|
// 权限申请
|
||||||
implementation(libs.accompanist.permissions)
|
implementation(libs.accompanist.permissions)
|
||||||
|
// hilt注入
|
||||||
|
implementation(libs.hilt.android)
|
||||||
|
ksp(libs.hilt.android.compiler)
|
||||||
|
|
||||||
|
// retrofit网络请求
|
||||||
|
implementation(libs.retrofit)
|
||||||
|
implementation(libs.converter.gson)
|
||||||
|
// okhttp
|
||||||
|
implementation(libs.okhttp)
|
||||||
|
implementation(libs.logging.interceptor)
|
||||||
|
// gson
|
||||||
|
implementation(libs.gson)
|
||||||
|
// 日志打印
|
||||||
|
implementation(libs.timber)
|
||||||
|
// 协程
|
||||||
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
|
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||||
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
|
||||||
|
implementation(libs.androidx.activity.ktx)
|
||||||
|
implementation(libs.androidx.fragment.ktx)
|
||||||
}
|
}
|
||||||
@@ -8,20 +8,36 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:usesCleartextTraffic="true"
|
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.DualScreen"
|
android:theme="@style/Theme.DualScreen"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".activity.Camera2Activity"
|
||||||
|
android:exported="true">
|
||||||
|
<!-- <intent-filter>-->
|
||||||
|
<!-- <action android:name="android.intent.action.MAIN" />-->
|
||||||
|
|
||||||
|
<!-- <category android:name="android.intent.category.LAUNCHER" />-->
|
||||||
|
<!-- </intent-filter>-->
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".activity.MainActivity"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.sw.dualscreen
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class App : Application() {
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
Timber.plant(Timber.DebugTree())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package com.sw.dualscreen
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import com.sw.dualscreen.databinding.ActivityMainBinding
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
|
||||||
lateinit var binding: ActivityMainBinding
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
enableEdgeToEdge()
|
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
|
||||||
setContentView(R.layout.activity_main)
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
|
||||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
|
||||||
insets
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package com.sw.dualscreen.activity
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.SurfaceTexture
|
||||||
|
import android.hardware.camera2.CameraCaptureSession
|
||||||
|
import android.hardware.camera2.CameraDevice
|
||||||
|
import android.hardware.camera2.CameraManager
|
||||||
|
import android.hardware.camera2.CaptureRequest
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Surface
|
||||||
|
import android.view.TextureView
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.sw.dualscreen.R
|
||||||
|
|
||||||
|
class Camera2Activity : AppCompatActivity() {
|
||||||
|
private lateinit var cameraManager: CameraManager
|
||||||
|
private var cameraIdList: Array<String> = arrayOf()
|
||||||
|
private var currentCameraIdIndex = 0
|
||||||
|
private lateinit var textureView: TextureView
|
||||||
|
private var cameraDevice: CameraDevice? = null
|
||||||
|
private var cameraCaptureSession: CameraCaptureSession? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_camera2)
|
||||||
|
|
||||||
|
textureView = findViewById(R.id.textureView)
|
||||||
|
cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||||
|
|
||||||
|
// 获取摄像头列表
|
||||||
|
cameraIdList = cameraManager.cameraIdList
|
||||||
|
if (cameraIdList.isEmpty()) {
|
||||||
|
Toast.makeText(this, "没有可用的摄像头", Toast.LENGTH_SHORT).show()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置TextureView监听
|
||||||
|
textureView.surfaceTextureListener = surfaceTextureListener
|
||||||
|
|
||||||
|
// 切换摄像头按钮
|
||||||
|
findViewById<Button>(R.id.switchButton).setOnClickListener {
|
||||||
|
switchCamera()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val surfaceTextureListener = object : TextureView.SurfaceTextureListener {
|
||||||
|
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
|
||||||
|
openCamera(cameraIdList[currentCameraIdIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSurfaceTextureSizeChanged(
|
||||||
|
surface: SurfaceTexture,
|
||||||
|
width: Int,
|
||||||
|
height: Int
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean = true
|
||||||
|
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openCamera(cameraId: String) {
|
||||||
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) !=
|
||||||
|
PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
|
||||||
|
override fun onOpened(device: CameraDevice) {
|
||||||
|
cameraDevice = device
|
||||||
|
createCameraPreviewSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisconnected(device: CameraDevice) {
|
||||||
|
device.close()
|
||||||
|
cameraDevice = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(device: CameraDevice, error: Int) {
|
||||||
|
device.close()
|
||||||
|
cameraDevice = null
|
||||||
|
}
|
||||||
|
}, null)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Camera2", "打开摄像头失败", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createCameraPreviewSession() {
|
||||||
|
val surfaceTexture = textureView.surfaceTexture
|
||||||
|
surfaceTexture?.setDefaultBufferSize(textureView.width, textureView.height)
|
||||||
|
val surface = Surface(surfaceTexture)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val previewRequestBuilder = cameraDevice?.createCaptureRequest(
|
||||||
|
CameraDevice.TEMPLATE_PREVIEW
|
||||||
|
)?.apply {
|
||||||
|
addTarget(surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraDevice?.createCaptureSession(
|
||||||
|
listOf(surface),
|
||||||
|
object : CameraCaptureSession.StateCallback() {
|
||||||
|
override fun onConfigured(session: CameraCaptureSession) {
|
||||||
|
cameraCaptureSession = session
|
||||||
|
previewRequestBuilder?.let { builder ->
|
||||||
|
builder.set(
|
||||||
|
CaptureRequest.CONTROL_AF_MODE,
|
||||||
|
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
|
||||||
|
)
|
||||||
|
session.setRepeatingRequest(builder.build(), null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigureFailed(session: CameraCaptureSession) {
|
||||||
|
Toast.makeText(this@Camera2Activity, "配置失败", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
null
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Camera2", "创建预览会话失败", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun switchCamera() {
|
||||||
|
if (cameraIdList.size <= 1) {
|
||||||
|
Toast.makeText(this, "只有一个摄像头", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭当前摄像头
|
||||||
|
cameraCaptureSession?.close()
|
||||||
|
cameraCaptureSession = null
|
||||||
|
cameraDevice?.close()
|
||||||
|
cameraDevice = null
|
||||||
|
|
||||||
|
// 切换到下一个摄像头
|
||||||
|
currentCameraIdIndex = (currentCameraIdIndex + 1) % cameraIdList.size
|
||||||
|
openCamera(cameraIdList[currentCameraIdIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
cameraCaptureSession?.close()
|
||||||
|
cameraDevice?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package com.sw.dualscreen.activity
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Outline
|
||||||
|
import android.hardware.display.DisplayManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewOutlineProvider
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.camera.core.CameraSelector
|
||||||
|
import androidx.camera.core.Preview
|
||||||
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
|
import com.sw.dualscreen.databinding.ActivityMainBinding
|
||||||
|
import com.sw.dualscreen.ext.dp
|
||||||
|
import com.sw.dualscreen.presentation.SecondaryScreenPresentation
|
||||||
|
import com.sw.dualscreen.viewmodel.MyViewModel
|
||||||
|
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
private lateinit var binding: ActivityMainBinding
|
||||||
|
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
|
||||||
|
private val viewModel = MyViewModel()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.main)
|
||||||
|
|
||||||
|
initView()
|
||||||
|
startCamera()
|
||||||
|
setupSecondaryDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
binding.previewView.outlineProvider = object : ViewOutlineProvider() {
|
||||||
|
override fun getOutline(view: View, outline: Outline) {
|
||||||
|
// outline.setRoundRect(0, 0, view.width, view.height, 12f.dp)
|
||||||
|
outline.setRoundRect(0, 0, view.width, view.height, 280f.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.previewView.clipToOutline = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun startCamera() {
|
||||||
|
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
|
||||||
|
cameraProviderFuture.addListener({
|
||||||
|
val cameraProvider = cameraProviderFuture.get()
|
||||||
|
|
||||||
|
// 创建预览用例
|
||||||
|
val preview = Preview.Builder()
|
||||||
|
.build()
|
||||||
|
.also {
|
||||||
|
it.setSurfaceProvider(binding.previewView.surfaceProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择后置摄像头
|
||||||
|
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 解绑所有用例
|
||||||
|
cameraProvider.unbindAll()
|
||||||
|
|
||||||
|
// 绑定到生命周期
|
||||||
|
cameraProvider.bindToLifecycle(
|
||||||
|
this, cameraSelector, preview
|
||||||
|
)
|
||||||
|
} catch (exc: Exception) {
|
||||||
|
Timber.e(exc, "Use case binding failed")
|
||||||
|
}
|
||||||
|
}, ContextCompat.getMainExecutor(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupSecondaryDisplay() {
|
||||||
|
val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
||||||
|
val displays = displayManager.displays
|
||||||
|
|
||||||
|
// 查找副屏(通常索引为1)
|
||||||
|
if (displays.size > 1) {
|
||||||
|
val secondaryDisplay = displays[1]
|
||||||
|
// 创建副屏的Presentation
|
||||||
|
val presentation =
|
||||||
|
SecondaryScreenPresentation(
|
||||||
|
activity = this,
|
||||||
|
this,
|
||||||
|
display = secondaryDisplay,
|
||||||
|
viewModel = viewModel,
|
||||||
|
recognizeViewModel = viewModels<RecognizeViewModel>().value
|
||||||
|
)
|
||||||
|
|
||||||
|
presentation.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.sw.dualscreen.adapter
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
|
||||||
|
abstract class BaseViewHolder<B : ViewBinding, T>(val binding: B) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
abstract fun bind(item: T)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.sw.dualscreen.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
|
||||||
|
class GridRecyclerAdapter<T, B : ViewBinding>(
|
||||||
|
private val itemList: List<T>,
|
||||||
|
private val bindingInflater: (LayoutInflater, ViewGroup, Boolean) -> B,
|
||||||
|
private val bindHolder: (B, T) -> Unit,
|
||||||
|
private val onItemClick: ((T) -> Unit)? = null
|
||||||
|
) : RecyclerView.Adapter<BaseViewHolder<B, T>>() {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<B, T> {
|
||||||
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
|
val binding = bindingInflater(inflater, parent, false)
|
||||||
|
return object : BaseViewHolder<B, T>(binding) {
|
||||||
|
override fun bind(item: T) = bindHolder(binding, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: BaseViewHolder<B, T>, position: Int) {
|
||||||
|
holder.bind(itemList[position])
|
||||||
|
holder.itemView.setOnClickListener {
|
||||||
|
onItemClick?.invoke(itemList[position])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = itemList.size
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.sw.dualscreen.adapter
|
||||||
|
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* item间距
|
||||||
|
*/
|
||||||
|
class GridSpacingItemDecoration(
|
||||||
|
private val spanCount: Int,
|
||||||
|
private val spacing: Int,
|
||||||
|
private val includeEdge: Boolean
|
||||||
|
) : RecyclerView.ItemDecoration() {
|
||||||
|
|
||||||
|
override fun getItemOffsets(
|
||||||
|
outRect: Rect,
|
||||||
|
view: View,
|
||||||
|
parent: RecyclerView,
|
||||||
|
state: RecyclerView.State
|
||||||
|
) {
|
||||||
|
val position = parent.getChildAdapterPosition(view)
|
||||||
|
val column = position % spanCount
|
||||||
|
|
||||||
|
if (includeEdge) {
|
||||||
|
outRect.left = spacing - column * spacing / spanCount
|
||||||
|
outRect.right = (column + 1) * spacing / spanCount
|
||||||
|
if (position < spanCount) outRect.top = spacing
|
||||||
|
outRect.bottom = spacing
|
||||||
|
} else {
|
||||||
|
outRect.left = column * spacing / spanCount
|
||||||
|
outRect.right = spacing - (column + 1) * spacing / spanCount
|
||||||
|
if (position >= spanCount) outRect.top = spacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.sw.dualscreen.ext
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.util.TypedValue
|
||||||
|
import androidx.annotation.Dimension
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Int 值转换为 dp 值
|
||||||
|
*/
|
||||||
|
val Int.dp: Int
|
||||||
|
@Dimension(unit = Dimension.DP)
|
||||||
|
get() = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
this.toFloat(),
|
||||||
|
Resources.getSystem().displayMetrics
|
||||||
|
).toInt()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Float 值转换为 dp 值
|
||||||
|
*/
|
||||||
|
val Float.dp: Float
|
||||||
|
@Dimension(unit = Dimension.DP)
|
||||||
|
get() = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
this,
|
||||||
|
Resources.getSystem().displayMetrics
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Int 值转换为 sp 值
|
||||||
|
*/
|
||||||
|
val Int.sp: Float
|
||||||
|
@Dimension(unit = Dimension.SP)
|
||||||
|
get() = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_SP,
|
||||||
|
this.toFloat(),
|
||||||
|
Resources.getSystem().displayMetrics
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Float 值转换为 sp 值
|
||||||
|
*/
|
||||||
|
val Float.sp: Float
|
||||||
|
@Dimension(unit = Dimension.SP)
|
||||||
|
get() = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_SP,
|
||||||
|
this,
|
||||||
|
Resources.getSystem().displayMetrics
|
||||||
|
)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.sw.dualscreen.ext
|
||||||
|
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.core.graphics.toColorInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置圆角边框
|
||||||
|
*/
|
||||||
|
fun View.setRoundedBorder(
|
||||||
|
@ColorInt solidColor: Int = "#19ECE7D0".toColorInt(), // 填充颜色
|
||||||
|
@ColorInt strokeColor: Int = "#ECE7D0".toColorInt(), // 边框颜色
|
||||||
|
strokeWidthDp: Float = 2f, // 边框宽度
|
||||||
|
cornerRadiusDp: Float = 24f, // 边框圆角
|
||||||
|
) {
|
||||||
|
background = GradientDrawable().apply {
|
||||||
|
shape = GradientDrawable.RECTANGLE
|
||||||
|
setColor(solidColor)
|
||||||
|
setStroke(
|
||||||
|
dpToPx(strokeWidthDp).toInt(),
|
||||||
|
strokeColor
|
||||||
|
)
|
||||||
|
this.cornerRadius = dpToPx(cornerRadiusDp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun View.dpToPx(dp: Float): Float {
|
||||||
|
return dp * context.resources.displayMetrics.density
|
||||||
|
}
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
package com.sw.dualscreen.presentation
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Presentation
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Point
|
||||||
|
import android.hardware.Camera
|
||||||
|
import android.hardware.camera2.CameraCaptureSession
|
||||||
|
import android.hardware.camera2.CameraDevice
|
||||||
|
import android.hardware.camera2.CameraManager
|
||||||
|
import android.hardware.camera2.CaptureRequest
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.DisplayMetrics
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Display
|
||||||
|
import android.view.Surface
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewTreeObserver
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import com.arcsoft.face.ErrorInfo
|
||||||
|
import com.sw.dualscreen.R
|
||||||
|
import com.sw.dualscreen.activity.MainActivity
|
||||||
|
import com.sw.dualscreen.databinding.PresentationSecondaryScreenBinding
|
||||||
|
import com.sw.dualscreen.viewmodel.MyViewModel
|
||||||
|
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.FacePreviewInfo
|
||||||
|
import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
|
||||||
|
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户屏幕
|
||||||
|
*/
|
||||||
|
class SecondaryScreenPresentation(
|
||||||
|
val activity: MainActivity,
|
||||||
|
context: Context,
|
||||||
|
display: Display,
|
||||||
|
viewModel: MyViewModel,
|
||||||
|
private val onDismissListener: () -> Unit = {},
|
||||||
|
val recognizeViewModel: RecognizeViewModel
|
||||||
|
) : Presentation(context, display), ViewTreeObserver.OnGlobalLayoutListener {
|
||||||
|
private lateinit var binding: PresentationSecondaryScreenBinding
|
||||||
|
private var cameraDevice: CameraDevice? = null
|
||||||
|
private var cameraCaptureSession: CameraCaptureSession? = null
|
||||||
|
private lateinit var cameraManager: CameraManager
|
||||||
|
private var cameraIdList: Array<String> = arrayOf()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
binding = PresentationSecondaryScreenBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
window?.setBackgroundDrawableResource(android.R.color.transparent)
|
||||||
|
|
||||||
|
cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||||
|
// 获取摄像头列表
|
||||||
|
cameraIdList = cameraManager.cameraIdList
|
||||||
|
if (cameraIdList.isEmpty()) {
|
||||||
|
Toast.makeText(context, "没有可用的摄像头", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
initArcViewModel()
|
||||||
|
initArcView()
|
||||||
|
openRectInfoDraw = true
|
||||||
|
openCamera(cameraIdList.last())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisplayRemoved() {
|
||||||
|
super.onDisplayRemoved()
|
||||||
|
onDismissListener()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openCamera(cameraId: String) {
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) !=
|
||||||
|
PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
|
||||||
|
override fun onOpened(device: CameraDevice) {
|
||||||
|
cameraDevice = device
|
||||||
|
// createCameraPreviewSession()
|
||||||
|
recognizeViewModel.init()
|
||||||
|
initRgbCamera()
|
||||||
|
resumeCamera()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisconnected(device: CameraDevice) {
|
||||||
|
device.close()
|
||||||
|
cameraDevice = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(device: CameraDevice, error: Int) {
|
||||||
|
device.close()
|
||||||
|
cameraDevice = null
|
||||||
|
}
|
||||||
|
}, null)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Camera2", "打开摄像头失败", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createCameraPreviewSession() {
|
||||||
|
val textureView = binding.dualCameraTexturePreviewRgb
|
||||||
|
val surfaceTexture = textureView.surfaceTexture
|
||||||
|
surfaceTexture?.setDefaultBufferSize(textureView.width, textureView.height)
|
||||||
|
val surface = Surface(surfaceTexture)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val previewRequestBuilder = cameraDevice?.createCaptureRequest(
|
||||||
|
CameraDevice.TEMPLATE_PREVIEW
|
||||||
|
)?.apply {
|
||||||
|
addTarget(surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraDevice?.createCaptureSession(
|
||||||
|
listOf(surface),
|
||||||
|
object : CameraCaptureSession.StateCallback() {
|
||||||
|
override fun onConfigured(session: CameraCaptureSession) {
|
||||||
|
cameraCaptureSession = session
|
||||||
|
previewRequestBuilder?.let { builder ->
|
||||||
|
builder.set(
|
||||||
|
CaptureRequest.CONTROL_AF_MODE,
|
||||||
|
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
|
||||||
|
)
|
||||||
|
session.setRepeatingRequest(builder.build(), null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigureFailed(session: CameraCaptureSession) {
|
||||||
|
Toast.makeText(context, "配置失败", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
null
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Camera2", "创建预览会话失败", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 虹软人脸配置 ⬇
|
||||||
|
private var isRecognition = false
|
||||||
|
private var rgbCameraHelper: DualCameraHelper? = null
|
||||||
|
private var rgbFaceRectTransformer: FaceRectTransformer? = null
|
||||||
|
private val livenessType = LivenessType.RGB
|
||||||
|
private var openRectInfoDraw = false
|
||||||
|
|
||||||
|
private fun initArcViewModel() {
|
||||||
|
|
||||||
|
recognizeViewModel.setLiveType(livenessType)
|
||||||
|
|
||||||
|
recognizeViewModel.ftInitCode.observe(activity, Observer { ftInitCode: Int? ->
|
||||||
|
if (ftInitCode != ErrorInfo.MOK) {
|
||||||
|
val error: String? = context.getString(
|
||||||
|
R.string.specific_engine_init_failed, "ftEngine",
|
||||||
|
ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode!!)
|
||||||
|
)
|
||||||
|
Timber.e("ftInitCode observe = $error")
|
||||||
|
ToastUtils.showToast(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
recognizeViewModel.frInitCode.observe(activity, Observer { frInitCode: Int? ->
|
||||||
|
if (frInitCode != ErrorInfo.MOK) {
|
||||||
|
val error: String? = context.getString(
|
||||||
|
R.string.specific_engine_init_failed, "frEngine",
|
||||||
|
frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode!!)
|
||||||
|
)
|
||||||
|
Timber.e("frInitCode observe = $error")
|
||||||
|
ToastUtils.showToast(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
recognizeViewModel.flInitCode.observe(activity, Observer { flInitCode: Int? ->
|
||||||
|
if (flInitCode != ErrorInfo.MOK) {
|
||||||
|
val error: String? = context.getString(
|
||||||
|
R.string.specific_engine_init_failed, "flEngine",
|
||||||
|
flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode!!)
|
||||||
|
)
|
||||||
|
Timber.e("flInitCode observe = $error")
|
||||||
|
ToastUtils.showToast(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
recognizeViewModel.recognizeConfiguration
|
||||||
|
.observe(activity, Observer { recognizeConfiguration: RecognizeConfiguration? ->
|
||||||
|
Timber.i("recognizeConfiguration: observe = ${recognizeConfiguration.toString()}")
|
||||||
|
})
|
||||||
|
recognizeViewModel.recognizeNotice.observe(activity, Observer { notice: String? ->
|
||||||
|
Timber.i("recognizeNotice observe notice = $notice")
|
||||||
|
})
|
||||||
|
|
||||||
|
recognizeViewModel.recognizeUserId.observe(activity, Observer { userId: String? ->
|
||||||
|
Timber.i("recognizeUserId observe userId = $userId")
|
||||||
|
// viewModel.getUserInfoById(memberId = userId?.toInt() ?: 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
recognizeViewModel.drawRectInfoText.observe(activity, Observer { info ->
|
||||||
|
Timber.i("drawRectInfoText observe info = $info")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initArcView() {
|
||||||
|
//在布局结束后才做初始化操作
|
||||||
|
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(this)
|
||||||
|
recognizeViewModel.getCompareResultList().getValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调整View的宽高,使预览显示正常且采集框固定为380x380
|
||||||
|
*
|
||||||
|
* @param rgbPreview RGB预览View
|
||||||
|
* @param previewView 显示预览数据的view
|
||||||
|
* @param faceRectView 画框的view (380x380)
|
||||||
|
* @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 layoutParams = previewView.layoutParams
|
||||||
|
val measuredWidth = previewView.measuredWidth
|
||||||
|
val measuredHeight = previewView.measuredHeight
|
||||||
|
var ratio = (previewSize.height.toFloat()) / previewSize.width.toFloat()
|
||||||
|
if (ratio > 1) {
|
||||||
|
ratio = 1 / ratio
|
||||||
|
}
|
||||||
|
if (displayOrientation % 180 == 0) {
|
||||||
|
layoutParams.width = measuredWidth
|
||||||
|
layoutParams.height = (measuredWidth * ratio).toInt()
|
||||||
|
} else {
|
||||||
|
layoutParams.height = measuredHeight
|
||||||
|
layoutParams.width = (measuredHeight * ratio).toInt()
|
||||||
|
}
|
||||||
|
if (scale < 1f) {
|
||||||
|
val rgbParam = rgbPreview.getLayoutParams()
|
||||||
|
layoutParams.width = (rgbParam.width * scale).toInt()
|
||||||
|
layoutParams.height = (rgbParam.height * scale).toInt()
|
||||||
|
} else {
|
||||||
|
layoutParams.width = (layoutParams.width * scale).toInt()
|
||||||
|
layoutParams.height = (layoutParams.height * scale).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
val metrics = DisplayMetrics()
|
||||||
|
activity.windowManager.defaultDisplay.getMetrics(metrics)
|
||||||
|
|
||||||
|
if (layoutParams.width >= metrics.widthPixels) {
|
||||||
|
val viewRatio = layoutParams.width / (metrics.widthPixels.toFloat())
|
||||||
|
layoutParams.width = (layoutParams.width / viewRatio).toInt()
|
||||||
|
layoutParams.height = (layoutParams.height / viewRatio).toInt()
|
||||||
|
}
|
||||||
|
if (layoutParams.height >= metrics.heightPixels) {
|
||||||
|
val viewRatio = layoutParams.height / (metrics.heightPixels.toFloat())
|
||||||
|
layoutParams.width = (layoutParams.width / viewRatio).toInt()
|
||||||
|
layoutParams.height = (layoutParams.height / viewRatio).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
activity.runOnUiThread({
|
||||||
|
val previewSizeRgb = camera.getParameters().getPreviewSize()
|
||||||
|
val layoutParams = adjustPreviewViewSize(
|
||||||
|
binding.dualCameraTexturePreviewRgb,
|
||||||
|
binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView,
|
||||||
|
previewSizeRgb, displayOrientation, 1f
|
||||||
|
)
|
||||||
|
Timber.d("initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}")
|
||||||
|
Timber.d("initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}")
|
||||||
|
rgbFaceRectTransformer = FaceRectTransformer(
|
||||||
|
previewSizeRgb.width,
|
||||||
|
previewSizeRgb.height,
|
||||||
|
layoutParams.width,
|
||||||
|
layoutParams.height,
|
||||||
|
displayOrientation,
|
||||||
|
cameraId,
|
||||||
|
isMirror,
|
||||||
|
ConfigUtil.isDrawRgbRectHorizontalMirror(context),
|
||||||
|
ConfigUtil.isDrawRgbRectVerticalMirror(context)
|
||||||
|
)
|
||||||
|
|
||||||
|
recognizeViewModel.onRgbCameraOpened(camera)
|
||||||
|
recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.Q)
|
||||||
|
override fun onPreview(nv21: ByteArray?, camera: Camera?) {
|
||||||
|
if (!isRecognition) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
binding.dualCameraFaceRectView.clearFaceInfo()
|
||||||
|
val facePreviewInfoList: MutableList<FacePreviewInfo?>? =
|
||||||
|
recognizeViewModel.onPreviewFrame(nv21, true)
|
||||||
|
if (facePreviewInfoList != null && rgbFaceRectTransformer != null) {
|
||||||
|
drawPreviewInfo(facePreviewInfoList)
|
||||||
|
}
|
||||||
|
recognizeViewModel.clearLeftFace(facePreviewInfoList)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCameraClosed() {
|
||||||
|
Timber.i("onCameraClosed: ")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCameraError(e: java.lang.Exception) {
|
||||||
|
Timber.i("onCameraError: %s", e.message)
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
|
||||||
|
Timber.i("onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}")
|
||||||
|
if (rgbFaceRectTransformer != null) {
|
||||||
|
rgbFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
|
||||||
|
}
|
||||||
|
Timber.i("onCameraConfigurationChanged: $cameraID $displayOrientation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val previewConfig: PreviewConfig = recognizeViewModel.previewConfig
|
||||||
|
rgbCameraHelper = DualCameraHelper.Builder()
|
||||||
|
.previewViewSize(
|
||||||
|
Point(
|
||||||
|
binding.dualCameraTexturePreviewRgb.measuredWidth,
|
||||||
|
binding.dualCameraTexturePreviewRgb.measuredHeight
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.rotation(activity.windowManager.defaultDisplay.rotation)
|
||||||
|
.additionalRotation(0) // 角度
|
||||||
|
.previewSize(recognizeViewModel.loadPreviewSize())
|
||||||
|
.specificCameraId(previewConfig.rgbCameraId)
|
||||||
|
.isMirror(true)
|
||||||
|
.previewOn(binding.dualCameraTexturePreviewRgb)
|
||||||
|
.cameraListener(cameraListener)
|
||||||
|
.build()
|
||||||
|
rgbCameraHelper!!.init()
|
||||||
|
rgbCameraHelper!!.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制RGB、IR画面的实时人脸信息
|
||||||
|
*
|
||||||
|
* @param facePreviewInfoList RGB画面的实时人脸信息
|
||||||
|
*/
|
||||||
|
private fun drawPreviewInfo(facePreviewInfoList: MutableList<FacePreviewInfo?>) {
|
||||||
|
if (rgbFaceRectTransformer != null) {
|
||||||
|
val rgbDrawInfoList: MutableList<DrawInfo?>? = recognizeViewModel.getDrawInfo(
|
||||||
|
facePreviewInfoList,
|
||||||
|
LivenessType.RGB,
|
||||||
|
openRectInfoDraw
|
||||||
|
)
|
||||||
|
// 识别成功
|
||||||
|
binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openRectInfoDraw(view: View?) {
|
||||||
|
openRectInfoDraw = !openRectInfoDraw
|
||||||
|
recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun onGlobalLayout() {
|
||||||
|
Timber.d("onGlobalLayout")
|
||||||
|
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// override fun onResume() {
|
||||||
|
// super.onResume()
|
||||||
|
// resumeCamera()
|
||||||
|
// viewModel.resetUserInfo()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// protected override fun onPause() {
|
||||||
|
// pauseCamera()
|
||||||
|
// super.onPause()
|
||||||
|
// }
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
if (rgbCameraHelper != null) {
|
||||||
|
rgbCameraHelper!!.release()
|
||||||
|
rgbCameraHelper = null
|
||||||
|
}
|
||||||
|
|
||||||
|
recognizeViewModel.destroy()
|
||||||
|
super.onStop()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resumeCamera() {
|
||||||
|
isRecognition = true
|
||||||
|
if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) {
|
||||||
|
rgbCameraHelper!!.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pauseCamera() {
|
||||||
|
isRecognition = false
|
||||||
|
|
||||||
|
recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.sw.dualscreen.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
|
||||||
|
class MyViewModel : ViewModel() {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
@@ -1,4 +1,24 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?><!--<layer-list xmlns:android="http://schemas.android.com/apk/res/android">-->
|
||||||
<selector>
|
|
||||||
|
|
||||||
</selector>
|
<!-- 圆形背景(可选) -->
|
||||||
|
<!-- <item>-->
|
||||||
|
<!-- <shape android:shape="oval">-->
|
||||||
|
<!-- <solid android:color="@android:color/transparent"/>-->
|
||||||
|
<!-- </shape>-->
|
||||||
|
<!-- </item>-->
|
||||||
|
|
||||||
|
<!-- 虚线边框 -->
|
||||||
|
<!--<item>-->
|
||||||
|
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:inset="0dp">
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<stroke
|
||||||
|
android:width="4dp"
|
||||||
|
android:color="#8C7D76"
|
||||||
|
android:dashWidth="8dp"
|
||||||
|
android:dashGap="4dp" />
|
||||||
|
<solid android:color="@android:color/transparent" />
|
||||||
|
</shape>
|
||||||
|
</inset><!--</item>-->
|
||||||
|
|
||||||
|
<!--</layer-list>-->
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<solid android:color="@color/transparent" />
|
||||||
|
|
||||||
|
<!-- 边框设置 -->
|
||||||
|
<stroke
|
||||||
|
android:width="2dp"
|
||||||
|
android:color="@color/white" />
|
||||||
|
|
||||||
|
<!-- 圆角半径 -->
|
||||||
|
<corners android:radius="0dp" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<solid android:color="@color/transparent" />
|
||||||
|
|
||||||
|
<!-- 边框设置 -->
|
||||||
|
<stroke
|
||||||
|
android:width="2dp"
|
||||||
|
android:color="#19ECE7D0" />
|
||||||
|
|
||||||
|
<!-- 圆角半径 -->
|
||||||
|
<corners android:radius="24dp" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<TextureView
|
||||||
|
android:id="@+id/textureView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/switchButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="切换摄像头"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="#000">
|
|
||||||
|
|
||||||
<!-- CameraX 预览视图 -->
|
|
||||||
<androidx.camera.view.PreviewView
|
|
||||||
android:id="@+id/previewView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:scaleType="fitCenter" />
|
|
||||||
|
|
||||||
<!-- 半透明遮罩层 -->
|
|
||||||
<View
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="#80000000" />
|
|
||||||
|
|
||||||
<!-- 自定义采集框 -->
|
|
||||||
<com.sw.dualscreen.view.CameraCaptureView
|
|
||||||
android:id="@+id/cameraCaptureView"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center" />
|
|
||||||
|
|
||||||
<!-- 底部操作栏 -->
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnCancel"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="取消"
|
|
||||||
android:textColor="#FFF" />
|
|
||||||
|
|
||||||
<Space
|
|
||||||
android:layout_width="16dp"
|
|
||||||
android:layout_height="1dp" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnCapture"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="拍照"
|
|
||||||
android:textColor="#FFF" />
|
|
||||||
</LinearLayout>
|
|
||||||
</FrameLayout>
|
|
||||||
@@ -1,10 +1,61 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/main"
|
android:id="@+id/main"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".MainActivity">
|
android:background="@mipmap/bg"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:context=".activity.MainActivity">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="80dp"
|
||||||
|
android:layout_marginTop="40dp">
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
<Spinner
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:layout_gravity="end|center_vertical" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/text_title_60_blob"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="羊肉臊子荞面饸饹" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="630dp"
|
||||||
|
android:layout_marginStart="120dp"
|
||||||
|
android:layout_marginTop="60dp"
|
||||||
|
android:layout_marginEnd="120dp"
|
||||||
|
android:layout_marginBottom="60dp"
|
||||||
|
android:visibility="visible">
|
||||||
|
|
||||||
|
<androidx.camera.view.PreviewView
|
||||||
|
android:id="@+id/previewView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_margin="30dp"
|
||||||
|
android:background="@drawable/preview_border" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/text_100"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="先识别 后取餐1" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/main"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@mipmap/bg"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:context=".activity.MainActivity">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="200dp"
|
||||||
|
android:layout_marginStart="120dp"
|
||||||
|
android:layout_marginTop="120dp"
|
||||||
|
android:layout_marginEnd="120dp"
|
||||||
|
android:background="@drawable/rounded_border_transparent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="visible">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_goods"
|
||||||
|
style="@style/text_title_60_blob"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="羊肉臊子荞面饸饹" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="560dp"
|
||||||
|
android:layout_marginTop="60dp"
|
||||||
|
android:visibility="visible">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="560dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:paddingTop="1dp">
|
||||||
|
|
||||||
|
<TextureView
|
||||||
|
android:id="@+id/dual_camera_texture_preview_rgb"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="visible" />
|
||||||
|
|
||||||
|
<com.sw.plate.utils.arcface.FaceRectView
|
||||||
|
android:id="@+id/dual_camera_face_rect_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="380dp" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:background="@drawable/bg_face" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="730dp"
|
||||||
|
android:layout_marginStart="120dp"
|
||||||
|
android:layout_marginEnd="120dp"
|
||||||
|
android:layout_marginBottom="120dp"
|
||||||
|
android:background="@drawable/rounded_border_transparent"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/text_100"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="先识别 后取餐" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 6.3 MiB |
@@ -6,4 +6,8 @@
|
|||||||
<attr name="borderWidth" format="dimension" />
|
<attr name="borderWidth" format="dimension" />
|
||||||
<attr name="borderColor" format="color" />
|
<attr name="borderColor" format="color" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
</resources>
|
|
||||||
|
<declare-styleable name="RoundedPreviewView">
|
||||||
|
<attr name="cornerRadius" format="dimension" />
|
||||||
|
</declare-styleable>
|
||||||
|
</resources>
|
||||||
|
|||||||
@@ -2,4 +2,9 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<color name="black">#FF000000</color>
|
<color name="black">#FF000000</color>
|
||||||
<color name="white">#FFFFFFFF</color>
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
<color name="transparent">#00000000</color>
|
||||||
|
<color name="origin_FF9900">#FF9900</color>
|
||||||
|
<color name="beige_ECE7D0">#ECE7D0</color>
|
||||||
|
<color name="blue_6498DD">#6498DD</color>
|
||||||
|
<color name="black_031127">#031127</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">DualScreen</string>
|
<string name="app_name">档口双屏</string>
|
||||||
|
<string name="specific_engine_init_failed">%s 初始化失败,错误码:%d\n错误码常量名:%s</string>
|
||||||
|
<string name="permission_denied">权限被拒绝!</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -6,4 +6,22 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.DualScreen" parent="Base.Theme.DualScreen" />
|
<style name="Theme.DualScreen" parent="Base.Theme.DualScreen" />
|
||||||
|
|
||||||
|
<style name="text_title_60_blob">
|
||||||
|
<item name="android:layout_width">match_parent</item>
|
||||||
|
<item name="android:textColor">@color/beige_ECE7D0</item>
|
||||||
|
<item name="android:textSize">60sp</item>
|
||||||
|
<item name="android:textStyle">bold</item>
|
||||||
|
<item name="singleLine">true</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="text_100">
|
||||||
|
<item name="android:layout_width">match_parent</item>
|
||||||
|
<item name="android:textColor">@color/origin_FF9900</item>
|
||||||
|
<item name="android:textSize">100sp</item>
|
||||||
|
<item name="android:textStyle">bold</item>
|
||||||
|
<item name="singleLine">true</item>
|
||||||
|
<item name="maxLines">1</item>
|
||||||
|
<item name="android:ellipsize">end</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,4 +2,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
alias(libs.plugins.kotlin.android) apply false
|
alias(libs.plugins.kotlin.android) apply false
|
||||||
|
id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
|
||||||
|
id("com.google.dagger.hilt.android") version "2.56.2" apply false
|
||||||
|
alias(libs.plugins.android.library) apply false
|
||||||
}
|
}
|
||||||
@@ -11,9 +11,19 @@ activity = "1.8.0"
|
|||||||
constraintlayout = "2.1.4"
|
constraintlayout = "2.1.4"
|
||||||
accompanistPermissions = "0.37.3"
|
accompanistPermissions = "0.37.3"
|
||||||
cameraCore = "1.3.0"
|
cameraCore = "1.3.0"
|
||||||
|
retrofit = "3.0.0"
|
||||||
|
okhttp = "4.12.0"
|
||||||
|
timber = "5.0.1"
|
||||||
|
gson = "2.13.1"
|
||||||
|
hiltAndroid = "2.56.2"
|
||||||
|
|
||||||
|
kotlinxCoroutinesAndroid = "1.6.4"
|
||||||
|
activityKtx = "1.9.0"
|
||||||
|
fragmentKtx = "1.5.6"
|
||||||
|
lifecycleViewmodelKtx = "2.8.3"
|
||||||
|
lifecycleRuntimeKtx = "2.8.3"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||||
@@ -29,7 +39,25 @@ androidx-camera-extensions = { module = "androidx.camera:camera-extensions", ver
|
|||||||
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "cameraCore" }
|
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "cameraCore" }
|
||||||
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "cameraCore" }
|
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "cameraCore" }
|
||||||
|
|
||||||
|
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
||||||
|
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" }
|
||||||
|
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
|
||||||
|
|
||||||
|
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
||||||
|
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||||
|
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||||
|
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
|
||||||
|
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
||||||
|
|
||||||
|
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
|
||||||
|
androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" }
|
||||||
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
|
androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" }
|
||||||
|
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||||
|
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
|
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.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")
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Vendored
+21
@@ -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
|
||||||
@@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
@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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
/**
|
||||||
|
* 方式二: 在激活界面读取本地配置文件进行激活
|
||||||
|
* <p>
|
||||||
|
* 配置文件名称,格式如下:
|
||||||
|
* APP_ID:XXXXXXXXXXXXX
|
||||||
|
* SDK_KEY:XXXXXXXXXXXXXXX
|
||||||
|
* ACTIVE_KEY:XXXX-XXXX-XXXX-XXXX
|
||||||
|
*/
|
||||||
|
public static final String ACTIVE_CONFIG_FILE_NAME = "activeConfig.txt";
|
||||||
|
}
|
||||||
@@ -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<PackageInfo> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打电话
|
||||||
|
* <p>
|
||||||
|
* 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地址
|
||||||
|
* 必须的权限 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"></uses-permission>
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
* 必须的权限 <uses-permission android:name="android.permission.INTERNET"></uses-permission>
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static String getMacFromHardware() {
|
||||||
|
try {
|
||||||
|
ArrayList<NetworkInterface> 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中加入权限即可
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Integer, Boolean> parseBoxStatus(String data) {
|
||||||
|
Map<Integer, Boolean> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.sw.plate.utils;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
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) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
Bitmap bmpout = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||||
|
out.copyTo(bmpout);
|
||||||
|
|
||||||
|
return bmpout;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package com.sw.plate.utils;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
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) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show center toast.
|
||||||
|
*
|
||||||
|
* @param text the text
|
||||||
|
*/
|
||||||
|
public static void showToast(String text, int duration) {
|
||||||
|
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) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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<CompareResult> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,463 @@
|
|||||||
|
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 = "400x640";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取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和业务代码
|
||||||
|
* <p>
|
||||||
|
* 获取识别界面是否保留最大人脸
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
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<FaceEntity> 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<FacePreviewInfo> onPreviewFrame(byte[] nv21, boolean doRecognize) {
|
||||||
|
|
||||||
|
// List<FacePreviewInfo> facePreviewInfoList = recognizeViewModel.onPreviewFrame(nv21, true);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
package com.sw.plate.utils.arcface;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
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<DrawInfo> 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<DrawInfo> faceInfoList) {
|
||||||
|
drawInfoList.addAll(faceInfoList);
|
||||||
|
postInvalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void drawRealtimeFaceInfo(List<DrawInfo> 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.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);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
+20
@@ -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 success 是否成功
|
||||||
|
*/
|
||||||
|
// void onRegisterFinished(FacePreviewInfo facePreviewInfo, boolean success);
|
||||||
|
void onRegisterFinished(FacePreviewInfo facePreviewInfo, UserFaceInfo success);
|
||||||
|
}
|
||||||
@@ -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<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
|
||||||
|
if (supportedPreviewSizes != null && supportedPreviewSizes.size() > 0) {
|
||||||
|
previewSize = getBestSupportedSize(supportedPreviewSizes, previewViewSize);
|
||||||
|
}
|
||||||
|
parameters.setPreviewSize(previewSize.width, previewSize.height);
|
||||||
|
|
||||||
|
// 对焦模式设置
|
||||||
|
List<String> 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<Camera.Size> 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<Camera.Size>() {
|
||||||
|
@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<Camera.Size> getSupportedPreviewSizes() {
|
||||||
|
if (mCamera == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return mCamera.getParameters().getSupportedPreviewSizes();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Camera.Size> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,555 @@
|
|||||||
|
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.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开两个相机的辅助类
|
||||||
|
* <p>
|
||||||
|
* 由于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<Camera.Size> rgbSupportedPreviewSizes;
|
||||||
|
private static List<Camera.Size> 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 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 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<Camera.Size> 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<Camera.Size> 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<Camera.Size> supportedPreviewSize = getCommonSupportedPreviewSize();
|
||||||
|
//相机数量为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();
|
||||||
|
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<String> 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<Camera.Size> 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<Camera.Size>() {
|
||||||
|
@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<Camera.Size> getSupportedPreviewSizes() {
|
||||||
|
if (mCamera == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return mCamera.getParameters().getSupportedPreviewSizes();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Camera.Size> 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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+98
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
|
}
|
||||||
+16
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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<Long>() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.sw.plate.utils.arcface.face.constants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测类型
|
||||||
|
*/
|
||||||
|
public enum LivenessType {
|
||||||
|
/**
|
||||||
|
* RGB活体检测
|
||||||
|
*/
|
||||||
|
RGB,
|
||||||
|
/**
|
||||||
|
* 红外活体检测
|
||||||
|
*/
|
||||||
|
IR
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
+29
@@ -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;
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package com.sw.plate.utils.arcface.face.constants;
|
||||||
|
|
||||||
|
public class RequestLivenessStatus {
|
||||||
|
public static final int ANALYZING = 10;
|
||||||
|
}
|
||||||
@@ -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<Integer, LinkedBlockingDeque<Rect>> 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<FacePreviewInfo> facePreviewInfoList) {
|
||||||
|
clearFacesNotInPreview(facePreviewInfoList);
|
||||||
|
for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) {
|
||||||
|
LinkedBlockingDeque<Rect> 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<Rect> 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> facePreviewInfo) {
|
||||||
|
Set<Integer> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -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<FacePreviewInfo> facePreviewInfoList) {
|
||||||
|
for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) {
|
||||||
|
if (!facePreviewInfo.isQualityPass()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
facePreviewInfo.setQualityPass(validArea.contains(facePreviewInfo.getRgbTransformedRect()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -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<FacePreviewInfo> facePreviewInfoList);
|
||||||
|
}
|
||||||
@@ -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<FacePreviewInfo> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+302
@@ -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()));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<FaceEntity> getAllFaces();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页获取库中的人脸
|
||||||
|
*
|
||||||
|
* @param start 起始下标
|
||||||
|
* @param size 单次获取的长度
|
||||||
|
* @return 从下标为start开始的size个已注册人脸
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM face order by faceId desc limit :start,:size ")
|
||||||
|
List<FaceEntity> 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)
|
||||||
|
void insert(List<FaceEntity> 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();
|
||||||
|
}
|
||||||
@@ -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<FaceEntity> CREATOR = new Creator<FaceEntity>() {
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<FaceEntity> 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<Integer>) emitter -> {
|
||||||
|
if (recognize) {
|
||||||
|
List<FaceEntity> 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<FaceEntity> 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<FaceEntity> faceEntityList) {
|
||||||
|
List<FaceFeatureInfo> 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<FaceInfo> 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<MaskInfo> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package com.sw.plate.utils.arcface.faceserver;
|
||||||
|
|
||||||
|
public class RegisterFailedException extends Exception {
|
||||||
|
public RegisterFailedException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 控制可识别区域的控件,中间的镂空区域为可识别区域。
|
||||||
|
* <p>
|
||||||
|
* 结合{@link FaceRectView}和{@link com.arcsoft.arcfacedemo.util.FaceRectTransformer}使用,可判断人脸是否显示在镂空区域
|
||||||
|
* <p>
|
||||||
|
* 注意:需要保证人脸框绘制正确,识别区域的控制才有效。
|
||||||
|
* <p>
|
||||||
|
* 实际使用中建议不要实现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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Integer> 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<Integer> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,658 @@
|
|||||||
|
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 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<List<CompareResult>> compareResultList;
|
||||||
|
|
||||||
|
private MutableLiveData<FaceItemEvent> faceItemEventMutableLiveData = new MutableLiveData<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 各个引擎初始化的错误码
|
||||||
|
*/
|
||||||
|
private MutableLiveData<Integer> ftInitCode = new MutableLiveData<>();
|
||||||
|
private MutableLiveData<Integer> frInitCode = new MutableLiveData<>();
|
||||||
|
private MutableLiveData<Integer> flInitCode = new MutableLiveData<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸操作辅助类,推帧即可,内部会进行特征提取、识别
|
||||||
|
*/
|
||||||
|
private FaceHelper faceHelper;
|
||||||
|
/**
|
||||||
|
* VIDEO模式人脸检测引擎,用于预览帧人脸追踪及图像质量检测
|
||||||
|
*/
|
||||||
|
private FaceEngine ftEngine;
|
||||||
|
/**
|
||||||
|
* 用于特征提取的引擎
|
||||||
|
*/
|
||||||
|
private FaceEngine frEngine;
|
||||||
|
/**
|
||||||
|
* IMAGE模式活体检测引擎,用于预览帧人脸活体检测
|
||||||
|
*/
|
||||||
|
private FaceEngine flEngine;
|
||||||
|
|
||||||
|
private PreviewConfig previewConfig;
|
||||||
|
|
||||||
|
private MutableLiveData<RecognizeConfiguration> recognizeConfiguration = new MutableLiveData<>();
|
||||||
|
|
||||||
|
private MutableLiveData<String> recognizeNotice = new MutableLiveData<>();
|
||||||
|
|
||||||
|
private MutableLiveData<String> drawRectInfoText = new MutableLiveData<>();
|
||||||
|
|
||||||
|
private MutableLiveData<String> 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<UserFaceInfo>) 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<UserFaceInfo>() {
|
||||||
|
|
||||||
|
@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<List<CompareResult>> getCompareResultList() {
|
||||||
|
if (compareResultList == null) {
|
||||||
|
compareResultList = new MutableLiveData<>();
|
||||||
|
compareResultList.setValue(new ArrayList<>());
|
||||||
|
}
|
||||||
|
return compareResultList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化引擎
|
||||||
|
*/
|
||||||
|
public void init() {
|
||||||
|
Context context = App.getContext();
|
||||||
|
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));
|
||||||
|
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<FacePreviewInfo> facePreviewInfoList) {
|
||||||
|
List<CompareResult> 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.getFaceEntity().getUserName());
|
||||||
|
}
|
||||||
|
boolean isAdded = false;
|
||||||
|
List<CompareResult> 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<Integer> getFtInitCode() {
|
||||||
|
return ftInitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MutableLiveData<Integer> getFrInitCode() {
|
||||||
|
return frInitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MutableLiveData<Integer> getFlInitCode() {
|
||||||
|
return flInitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MutableLiveData<String> getRecognizeNotice() {
|
||||||
|
return recognizeNotice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MutableLiveData<String> getRecognizeUserId() {
|
||||||
|
return recognizeUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MutableLiveData<String> getDrawRectInfoText() {
|
||||||
|
return drawRectInfoText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MutableLiveData<FaceItemEvent> getFaceItemEventMutableLiveData() {
|
||||||
|
return faceItemEventMutableLiveData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 准备注册,将注册的状态值修改为待注册
|
||||||
|
*/
|
||||||
|
public void prepareRegister() {
|
||||||
|
if (registerStatus == REGISTER_STATUS_DONE) {
|
||||||
|
updateRegisterStatus(REGISTER_STATUS_READY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateRegisterStatus(int status) {
|
||||||
|
registerStatus = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据预览信息生成绘制信息
|
||||||
|
*
|
||||||
|
* @param facePreviewInfoList 预览信息
|
||||||
|
* @return 绘制信息
|
||||||
|
*/
|
||||||
|
public List<FaceRectView.DrawInfo> getDrawInfo(List<FacePreviewInfo> facePreviewInfoList, LivenessType livenessType, boolean drawRectInfo) {
|
||||||
|
List<FaceRectView.DrawInfo> 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;
|
||||||
|
}
|
||||||
|
if (recognizeStatus == RequestFeatureStatus.SUCCEED) {
|
||||||
|
color = RecognizeColor.COLOR_SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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<FacePreviewInfo> onPreviewFrame(byte[] nv21, boolean doRecognize) {
|
||||||
|
if (faceHelper != null) {
|
||||||
|
if (!loadFaceList) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (livenessType == LivenessType.IR && irNV21 == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<FacePreviewInfo> 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<RecognizeConfiguration> 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]));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.sw.plate.utils.comn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 串口设备
|
||||||
|
*/
|
||||||
|
public class Device {
|
||||||
|
|
||||||
|
private String path;
|
||||||
|
private String baudrate;
|
||||||
|
|
||||||
|
public Device() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Device(String path, String baudrate) {
|
||||||
|
this.path = path;
|
||||||
|
this.baudrate = baudrate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPath() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPath(String path) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBaudrate() {
|
||||||
|
return baudrate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBaudrate(String baudrate) {
|
||||||
|
this.baudrate = baudrate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Device{" + "path='" + path + '\'' + ", baudrate='" + baudrate + '\'' + '}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.sw.plate.utils.comn;
|
||||||
|
|
||||||
|
import static com.sw.plate.utils.CabinetLockCommand.generateOpenCommand;
|
||||||
|
|
||||||
|
import android.serialport.SerialPort;
|
||||||
|
|
||||||
|
public class SerialApi {
|
||||||
|
private static String path = "/dev/ttyS2";
|
||||||
|
private static int speed = 19200;
|
||||||
|
private static SerialPortManager serialPortManager;
|
||||||
|
private static SerialPort serialPort;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
serialPortManager = SerialPortManager.instance();
|
||||||
|
serialPort = serialPortManager.open(new Device(path, String.valueOf(speed)));
|
||||||
|
// if (serialPort == null) {
|
||||||
|
// ToastUtils.showToast("打开串口失败");
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开柜
|
||||||
|
*
|
||||||
|
* @param boxNumber
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
public static void openPlate(int boxNumber, SerialPortManager.SendCallback callback) {
|
||||||
|
if (serialPort == null) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
if (serialPort == null) {
|
||||||
|
callback.onFail(new Exception("打开串口失败"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
serialPortManager.sendCommand(generateOpenCommand(boxNumber), callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
package com.sw.plate.utils.comn;
|
||||||
|
|
||||||
|
import android.os.HandlerThread;
|
||||||
|
import android.serialport.SerialPort;
|
||||||
|
|
||||||
|
import com.sw.plate.utils.ByteUtil;
|
||||||
|
import com.sw.plate.utils.L;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
import io.reactivex.Observable;
|
||||||
|
import io.reactivex.ObservableEmitter;
|
||||||
|
import io.reactivex.ObservableOnSubscribe;
|
||||||
|
import io.reactivex.Observer;
|
||||||
|
import io.reactivex.Scheduler;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.disposables.Disposable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Administrator on 2017/3/28 0028.
|
||||||
|
*/
|
||||||
|
public class SerialPortManager {
|
||||||
|
|
||||||
|
private static final String TAG = "SerialPortManager";
|
||||||
|
|
||||||
|
private SerialReadThread mReadThread;
|
||||||
|
private OutputStream mOutputStream;
|
||||||
|
private HandlerThread mWriteThread;
|
||||||
|
private Scheduler mSendScheduler;
|
||||||
|
|
||||||
|
private static class InstanceHolder {
|
||||||
|
|
||||||
|
public static SerialPortManager sManager = new SerialPortManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SerialPortManager instance() {
|
||||||
|
return InstanceHolder.sManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SerialPort mSerialPort;
|
||||||
|
|
||||||
|
private SerialPortManager() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开串口
|
||||||
|
*
|
||||||
|
* @param device
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public SerialPort open(Device device) {
|
||||||
|
return open(device.getPath(), device.getBaudrate());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开串口
|
||||||
|
*
|
||||||
|
* @param devicePath
|
||||||
|
* @param baudrateString
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public SerialPort open(String devicePath, String baudrateString) {
|
||||||
|
if (mSerialPort != null) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
File device = new File(devicePath);
|
||||||
|
int baurate = Integer.parseInt(baudrateString);
|
||||||
|
mSerialPort = new SerialPort(device, baurate);
|
||||||
|
|
||||||
|
mReadThread = new SerialReadThread(mSerialPort.getInputStream());
|
||||||
|
mReadThread.start();
|
||||||
|
|
||||||
|
mOutputStream = mSerialPort.getOutputStream();
|
||||||
|
|
||||||
|
mWriteThread = new HandlerThread("write-thread");
|
||||||
|
mWriteThread.start();
|
||||||
|
mSendScheduler = AndroidSchedulers.from(mWriteThread.getLooper());
|
||||||
|
L.e("串口打开成功");
|
||||||
|
return mSerialPort;
|
||||||
|
} catch (Throwable tr) {
|
||||||
|
L.e("打开串口失败" + tr);
|
||||||
|
close();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭串口
|
||||||
|
*/
|
||||||
|
public void close() {
|
||||||
|
if (mReadThread != null) {
|
||||||
|
mReadThread.close();
|
||||||
|
}
|
||||||
|
if (mOutputStream != null) {
|
||||||
|
try {
|
||||||
|
mOutputStream.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mWriteThread != null) {
|
||||||
|
mWriteThread.quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mSerialPort != null) {
|
||||||
|
mSerialPort.close();
|
||||||
|
mSerialPort = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送数据
|
||||||
|
*
|
||||||
|
* @param datas
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private void sendData(byte[] datas) throws Exception {
|
||||||
|
mOutputStream.write(datas);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (rx包裹)发送数据
|
||||||
|
*
|
||||||
|
* @param datas
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private Observable<Object> rxSendData(final byte[] datas) {
|
||||||
|
|
||||||
|
return Observable.create(new ObservableOnSubscribe<Object>() {
|
||||||
|
@Override
|
||||||
|
public void subscribe(ObservableEmitter<Object> emitter) throws Exception {
|
||||||
|
try {
|
||||||
|
sendData(datas);
|
||||||
|
emitter.onNext(new Object());
|
||||||
|
} catch (Exception e) {
|
||||||
|
L.e("发送:" + ByteUtil.bytes2HexStr(datas) + " 失败===" + e);
|
||||||
|
|
||||||
|
if (!emitter.isDisposed()) {
|
||||||
|
emitter.onError(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emitter.onComplete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送命令包
|
||||||
|
*/
|
||||||
|
public void sendCommand(final String command, SendCallback callback) {
|
||||||
|
|
||||||
|
// TODO: 2018/3/22
|
||||||
|
L.e("发送命令:" + command);
|
||||||
|
|
||||||
|
byte[] bytes = ByteUtil.hexStr2bytes(command);
|
||||||
|
rxSendData(bytes).subscribeOn(mSendScheduler).subscribe(new Observer<Object>() {
|
||||||
|
@Override
|
||||||
|
public void onSubscribe(Disposable d) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNext(Object o) {
|
||||||
|
// LogManager.instance().post(new SendMessage(command));
|
||||||
|
callback.onSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Throwable e) {
|
||||||
|
L.e("发送失败" + e);
|
||||||
|
callback.onFail(new Exception(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComplete() {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface SendCallback {
|
||||||
|
void onSuccess();
|
||||||
|
|
||||||
|
void onFail(Exception e);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package com.sw.plate.utils.comn;
|
||||||
|
|
||||||
|
import static com.sw.plate.utils.CabinetLockCommand.parseBoxStatus;
|
||||||
|
|
||||||
|
import android.os.SystemClock;
|
||||||
|
|
||||||
|
import com.sw.plate.utils.ByteUtil;
|
||||||
|
import com.sw.plate.utils.L;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读串口线程
|
||||||
|
*/
|
||||||
|
public class SerialReadThread extends Thread {
|
||||||
|
|
||||||
|
private static final String TAG = "SerialReadThread";
|
||||||
|
|
||||||
|
private BufferedInputStream mInputStream;
|
||||||
|
|
||||||
|
public SerialReadThread(InputStream is) {
|
||||||
|
mInputStream = new BufferedInputStream(is);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
byte[] received = new byte[1024];
|
||||||
|
int size;
|
||||||
|
|
||||||
|
L.e("开始读线程");
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
|
||||||
|
if (Thread.currentThread().isInterrupted()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
|
||||||
|
int available = mInputStream.available();
|
||||||
|
|
||||||
|
if (available > 0) {
|
||||||
|
size = mInputStream.read(received);
|
||||||
|
if (size > 0) {
|
||||||
|
onDataReceive(received, size);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 暂停一点时间,免得一直循环造成CPU占用率过高
|
||||||
|
SystemClock.sleep(1);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
L.e("读取数据失败" + e);
|
||||||
|
}
|
||||||
|
//Thread.yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
L.e("结束读进程");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理获取到的数据
|
||||||
|
*
|
||||||
|
* @param received
|
||||||
|
* @param size
|
||||||
|
*/
|
||||||
|
private void onDataReceive(byte[] received, int size) {
|
||||||
|
// TODO: 2018/3/22 解决粘包、分包等
|
||||||
|
String hexStr = ByteUtil.bytes2HexStr(received, 0, size);
|
||||||
|
// LogManager.instance().post(new RecvMessage(hexStr));
|
||||||
|
L.e("接收数据:" + hexStr);
|
||||||
|
if (hexStr.startsWith("5AA2")) {
|
||||||
|
Map<Integer, Boolean> statusMap = parseBoxStatus(hexStr);
|
||||||
|
for (Map.Entry<Integer, Boolean> entry : statusMap.entrySet()) {
|
||||||
|
System.out.println("箱门" + entry.getKey() + ": " +
|
||||||
|
(entry.getValue() ? "开" : "关"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止读线程
|
||||||
|
*/
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
mInputStream.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
L.e("异常" + e);
|
||||||
|
} finally {
|
||||||
|
super.interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.sw.plate.utils.comn.message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志消息数据接口
|
||||||
|
*/
|
||||||
|
|
||||||
|
public interface IMessage {
|
||||||
|
/**
|
||||||
|
* 消息文本
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
String getMessage();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否发送的消息
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
boolean isToSend();
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.sw.plate.utils.comn.message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收到的日志
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class RecvMessage implements IMessage {
|
||||||
|
|
||||||
|
private String command;
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
public RecvMessage(String command) {
|
||||||
|
this.command = command;
|
||||||
|
this.message = " 收到命令:" + command;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isToSend() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user