This commit is contained in:
zxj
2025-07-29 11:09:59 +08:00
parent df5acf6b61
commit 56e0a85a4c
106 changed files with 10986 additions and 92 deletions
+27 -1
View File
@@ -1,6 +1,9 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
id("com.google.devtools.ksp")
id("com.google.dagger.hilt.android")
id("kotlin-parcelize")
}
android {
@@ -9,7 +12,7 @@ android {
defaultConfig {
applicationId = "com.sw.dualscreen"
minSdk = 24
minSdk = 29
targetSdk = 35
versionCode = 1
versionName = "1.0"
@@ -49,6 +52,8 @@ dependencies {
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
implementation(project(":lib_face"))
// camerax
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
@@ -58,4 +63,25 @@ dependencies {
// 权限申请
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)
}
+18 -2
View File
@@ -8,20 +8,36 @@
<uses-permission android:name="android.permission.INTERNET" />
<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
android:name=".App"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.DualScreen"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<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">
<intent-filter>
<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"?>
<selector>
<?xml version="1.0" encoding="utf-8"?><!--<layer-list xmlns:android="http://schemas.android.com/apk/res/android">-->
</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>
+54 -3
View File
@@ -1,10 +1,61 @@
<?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"
android:id="@+id/main"
android:layout_width="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

+5 -1
View File
@@ -6,4 +6,8 @@
<attr name="borderWidth" format="dimension" />
<attr name="borderColor" format="color" />
</declare-styleable>
</resources>
<declare-styleable name="RoundedPreviewView">
<attr name="cornerRadius" format="dimension" />
</declare-styleable>
</resources>
+5
View File
@@ -2,4 +2,9 @@
<resources>
<color name="black">#FF000000</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>
+3 -1
View File
@@ -1,3 +1,5 @@
<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>
+18
View File
@@ -6,4 +6,22 @@
</style>
<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>
+3
View File
@@ -2,4 +2,7 @@
plugins {
alias(libs.plugins.android.application) 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
}
+29 -1
View File
@@ -11,9 +11,19 @@ activity = "1.8.0"
constraintlayout = "2.1.4"
accompanistPermissions = "0.37.3"
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]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
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-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]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "agp" }
+1
View File
@@ -0,0 +1 @@
/build
+67
View File
@@ -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")
}
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+21
View File
@@ -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());
}
}
+4
View File
@@ -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);
}
@@ -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;
}
}
@@ -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
* * *
* * *
* * *
* * *
* * *
* 11***********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);
}
@@ -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;
}
@@ -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;
}
@@ -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);
}
}
@@ -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()));
}
}
}
@@ -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;
}
}
@@ -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.REGISTERmask的值为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.REGISTERmask的值为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;
}
}
@@ -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