This commit is contained in:
zxj
2025-07-16 08:56:21 +08:00
commit 9669872d23
109 changed files with 9252 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
+3
View File
@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="SmartPlateCabinet.app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/lib_face" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+1
View File
@@ -0,0 +1 @@
/build
+56
View File
@@ -0,0 +1,56 @@
import org.gradle.kotlin.dsl.implementation
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
id("com.google.devtools.ksp")
id("kotlin-parcelize")
}
android {
namespace = "com.sw.platecabinet"
compileSdk = 35
defaultConfig {
applicationId = "com.sw.platecabinet"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/))
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(project(":lib_face"))
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
+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,24 @@
package com.sw.platecabinet
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.sw.platecabinet", appContext.packageName)
}
}
+27
View File
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".MyApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SmartPlateCabinet"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,20 @@
package com.sw.platecabinet
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
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,9 @@
package com.sw.platecabinet
import android.app.Application
import android.content.Context
import com.sw.plate.App
class MyApp : App() {
}
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
+19
View File
@@ -0,0 +1,19 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

+7
View File
@@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.SmartPlateCabinet" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="color_bg_notification">#80000000</color>
</resources>
+3
View File
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">SmartPlateCabinet</string>
</resources>
+9
View File
@@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.SmartPlateCabinet" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Theme.SmartPlateCabinet" parent="Base.Theme.SmartPlateCabinet" />
</resources>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
@@ -0,0 +1,17 @@
package com.sw.platecabinet
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
+7
View File
@@ -0,0 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
alias(libs.plugins.android.library) apply false
}
+23
View File
@@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
+27
View File
@@ -0,0 +1,27 @@
[versions]
agp = "8.10.1"
kotlin = "2.0.21"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
appcompat = "1.6.1"
material = "1.10.0"
activity = "1.8.0"
constraintlayout = "2.1.4"
[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" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
[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" }
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
#Tue Jul 15 15:15:07 CST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
#distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.11.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
Vendored
+89
View File
@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+1
View File
@@ -0,0 +1 @@
/build
+70
View File
@@ -0,0 +1,70 @@
import org.gradle.kotlin.dsl.annotationProcessor
import org.gradle.kotlin.dsl.implementation
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 {
implementation(
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
+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 android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* 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 xmlns:android="http://schemas.android.com/apk/res/android">
</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,31 @@
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,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,229 @@
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,67 @@
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,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和业务代码
*
* 获取识别界面是否保留最大人脸
*
* @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,50 @@
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,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,40 @@
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,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,28 @@
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,301 @@
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;
import java.io.File;
@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.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,623 @@
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.os.Environment;
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";
}
/**
* 根据用户名获取注册图保存路径
*
* @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, "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 + '\'' + '}';
}
}

Some files were not shown because too many files have changed in this diff Show More