diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4c3a57 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +*.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 +/.idea/ diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..df168a2 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,65 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.kbyai.faceattribute' + compileSdk 34 + + defaultConfig { + applicationId "com.kbyai.activelive" + minSdk 24 + targetSdk 34 + versionCode 5 + versionName "1.4" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + ndk { + abiFilters 'arm64-v8a', 'armeabi-v7a' + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.8.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.preference:preference:1.2.0' + implementation 'androidx.preference:preference-ktx:1.2.0' + + implementation "androidx.camera:camera-core:1.0.0-beta12" + implementation "androidx.camera:camera-camera2:1.0.0-beta12" + implementation "androidx.camera:camera-lifecycle:1.0.0-beta12" + implementation 'androidx.camera:camera-view:1.0.0-alpha19' + + implementation 'com.squareup.okhttp3:okhttp:4.9.3' + implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1' + implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.1' + + implementation project(path: ':libfacesdk') + + // implementation 'io.fotoapparat:fotoapparat:2.7.0' + implementation project(path: ':libfotoapparat') + + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/kbyai/faceattribute/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/kbyai/faceattribute/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..b619f44 --- /dev/null +++ b/app/src/androidTest/java/com/kbyai/faceattribute/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.kbyai.faceattribute + +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.kbyai.faceattribute", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f9c9bd2 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/kbyai/faceattribute/AboutActivity.kt b/app/src/main/java/com/kbyai/faceattribute/AboutActivity.kt new file mode 100644 index 0000000..fa7515f --- /dev/null +++ b/app/src/main/java/com/kbyai/faceattribute/AboutActivity.kt @@ -0,0 +1,149 @@ +package com.kbyai.faceattribute + +import android.content.Intent +import android.content.pm.ResolveInfo +import android.net.Uri +import android.os.Bundle +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity + + +class AboutActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_about) + + findViewById(R.id.txtMail).setOnClickListener { + val intent = Intent(Intent.ACTION_SEND) + intent.type = "plain/text" + intent.putExtra(Intent.EXTRA_EMAIL, arrayOf("contact@kby-ai.com")) + intent.putExtra(Intent.EXTRA_SUBJECT, "License Request") + intent.putExtra(Intent.EXTRA_TEXT, "") + startActivity(Intent.createChooser(intent, "")) + } + + findViewById(R.id.txtWhatsapp).setOnClickListener { + val general = Intent(Intent.ACTION_VIEW, Uri.parse("https://com.whatsapp/kbyai")) + val generalResolvers: HashSet = HashSet() + val generalResolveInfo: List = packageManager.queryIntentActivities(general, 0) + for (info in generalResolveInfo) { + if (info.activityInfo.packageName != null) { + generalResolvers.add(info.activityInfo.packageName) + } + } + + val telegram = Intent(Intent.ACTION_VIEW, Uri.parse("https://wa.me/19092802609")) + var goodResolver = 0 + + val resInfo: List = packageManager.queryIntentActivities(telegram, 0) + if (!resInfo.isEmpty()) { + for (info in resInfo) { + if (info.activityInfo.packageName != null && !generalResolvers.contains(info.activityInfo.packageName)) { + goodResolver++ + telegram.setPackage(info.activityInfo.packageName) + } + } + } + + if (goodResolver != 1) { + telegram.setPackage(null) + } + if (telegram.resolveActivity(packageManager) != null) { + startActivity(telegram) + } + } + + findViewById(R.id.txtTelegram).setOnClickListener { + val general = Intent(Intent.ACTION_VIEW, Uri.parse("https://t.com/kbyai")) + val generalResolvers: HashSet = HashSet() + val generalResolveInfo: List = packageManager.queryIntentActivities(general, 0) + for (info in generalResolveInfo) { + if (info.activityInfo.packageName != null) { + generalResolvers.add(info.activityInfo.packageName) + } + } + + val telegram = Intent(Intent.ACTION_VIEW, Uri.parse("https://t.me/kbyai")) + var goodResolver = 0 + + val resInfo: List = packageManager.queryIntentActivities(telegram, 0) + if (!resInfo.isEmpty()) { + for (info in resInfo) { + if (info.activityInfo.packageName != null && !generalResolvers.contains(info.activityInfo.packageName)) { + goodResolver++ + telegram.setPackage(info.activityInfo.packageName) + } + } + } + + if (goodResolver != 1) { + telegram.setPackage(null) + } + if (telegram.resolveActivity(packageManager) != null) { + startActivity(telegram) + } + } + + findViewById(R.id.txtSkype).setOnClickListener { + val general = Intent(Intent.ACTION_VIEW, Uri.parse("https://com.skype/kbyai")) + val generalResolvers: HashSet = HashSet() + val generalResolveInfo: List = packageManager.queryIntentActivities(general, 0) + for (info in generalResolveInfo) { + if (info.activityInfo.packageName != null) { + generalResolvers.add(info.activityInfo.packageName) + } + } + + val telegram = Intent(Intent.ACTION_VIEW, Uri.parse("https://join.skype.com/invite/OffY2r1NUFev")) + var goodResolver = 0 + + val resInfo: List = packageManager.queryIntentActivities(telegram, 0) + if (!resInfo.isEmpty()) { + for (info in resInfo) { + if (info.activityInfo.packageName != null && !generalResolvers.contains(info.activityInfo.packageName)) { + goodResolver++ + telegram.setPackage(info.activityInfo.packageName) + } + } + } + + if (goodResolver != 1) { + telegram.setPackage(null) + } + if (telegram.resolveActivity(packageManager) != null) { + startActivity(telegram) + } + } + + findViewById(R.id.txtGitHub).setOnClickListener { + val general = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/kby-ai")) + val generalResolvers: HashSet = HashSet() + val generalResolveInfo: List = packageManager.queryIntentActivities(general, 0) + for (info in generalResolveInfo) { + if (info.activityInfo.packageName != null) { + generalResolvers.add(info.activityInfo.packageName) + } + } + + val telegram = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/kby-ai")) + var goodResolver = 0 + + val resInfo: List = packageManager.queryIntentActivities(telegram, 0) + if (!resInfo.isEmpty()) { + for (info in resInfo) { + if (info.activityInfo.packageName != null && !generalResolvers.contains(info.activityInfo.packageName)) { + goodResolver++ + telegram.setPackage(info.activityInfo.packageName) + } + } + } + + if (goodResolver != 1) { + telegram.setPackage(null) + } + if (telegram.resolveActivity(packageManager) != null) { + startActivity(telegram) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbyai/faceattribute/CameraActivity.kt b/app/src/main/java/com/kbyai/faceattribute/CameraActivity.kt new file mode 100644 index 0000000..0b30664 --- /dev/null +++ b/app/src/main/java/com/kbyai/faceattribute/CameraActivity.kt @@ -0,0 +1,1385 @@ +package com.kbyai.faceattribute + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.* +import android.os.* +import android.util.Log +import android.util.Size +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.preference.PreferenceManager +import com.kbyai.faceattribute.FaceRectTransformer +import com.kbyai.faceattribute.FaceRectView +import com.kbyai.faceattribute.R +import com.kbyai.faceattribute.SettingsActivity +import com.kbyai.faceattribute.Utils +import com.kbyai.facesdk.FaceSDK +import com.kbyai.facesdk.FaceBox +import com.kbyai.facesdk.FaceDetectionParam + +import io.fotoapparat.Fotoapparat +import io.fotoapparat.parameter.Resolution +import io.fotoapparat.preview.Frame +import io.fotoapparat.selector.front +import io.fotoapparat.selector.highestResolution +import io.fotoapparat.util.FrameProcessor +import io.fotoapparat.view.CameraView +import okhttp3.* +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Request.* +import okhttp3.logging.HttpLoggingInterceptor +import java.io.* +import java.security.KeyStore +import java.security.SecureRandom +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.util.concurrent.ExecutorService +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager +import kotlin.concurrent.thread +import kotlin.math.acos +import kotlin.math.sqrt + + +class CameraActivity : AppCompatActivity() { + + private enum class State { + IDLE, + START_LOOK_UP, + LOOK_UP, + START_NOD, + NOD, + START_LOOK_UP_NOD, + LOOK_UP_NOD, + START_NOD_LOOK_UP, + NOD_LOOK_UP, + START_ZOOM_IN, + ZOOM_IN, + START_ZOOM_OUT, + ZOOM_OUT, + START_ZOOM_IN_OUT, + ZOOM_IN_OUT, + START_ZOOM_OUT_IN, + ZOOM_OUT_IN, + START_MOUTH, + MOUTH, + START_EYE_BLINK, + EYE_BLINK, + START_TURN_LEFT, + TURN_LEFT, + START_TURN_RIGHT, + TURN_RIGHT, + START_TURN_LEFT_RIGHT, + TURN_LEFT_RIGHT, + START_TURN_RIGHT_LEFT, + TURN_RIGHT_LEFT, + LIVENESS_CHECK_COMPLETED, + END + } + + public enum class ROI_CHECK_RESULT { + ROI_NO_FACE, + ROI_INTERSECTS, + ROI_SMALL_FACE, + ROI_FACE_OK + } + + companion object { + private val TAG = CameraActivity::class.simpleName + private const val ALLOW_NO_FACE_TIMES = 2 + private const val FACING_CAMERA_KEEP_TIME = 3000L + + val PREVIEW_WIDTH = 720 + val PREVIEW_HEIGHT = 1280 + + const val RESULT_KEY_FACING_CAMERA_IMAGE_PATH = "facing_image" + const val RESULT_KEY_SMILING_IMAGE_PATH = "smiling_image" + const val RESULT_KEY_MOUTH_IMAGE_PATH = "mouth_image" + const val RESULT_KEY_SHAKE_IMAGE_PATH = "shake_image" + + const val HANDLE_UPDATE_FACE = 0 + const val HANDLE_TOAST_SHOW = 1 + const val HANDLE_SET_TITLE = 2 + const val HANDLE_VIEW_MODE = 3 + const val HANDLE_START_TIMER = 4 + const val HANDLE_STOP_TIMER = 5 + const val HANDLE_VIBRATOR = 6 + } + + private lateinit var imageScene: ImageView + private lateinit var txtWarning: TextView + + private var warningMessage = "" + private val actionsList: List = listOf(State.START_LOOK_UP, State.START_NOD, State.START_LOOK_UP_NOD, State.START_NOD_LOOK_UP, + State.START_ZOOM_IN, State.START_ZOOM_OUT, State.START_ZOOM_IN_OUT, State.START_ZOOM_OUT_IN, + State.START_MOUTH, State.START_EYE_BLINK, State.START_TURN_LEFT, State.START_TURN_RIGHT, State.START_TURN_LEFT_RIGHT, State.START_TURN_RIGHT_LEFT) + private var actionsIdxs = ArrayList() + private var currentActionIdx = 0 + private var context: Context? = null + private var cameraView: CameraView? = null + private var textView:TextView?=null + private var txtTimer: TextView?= null + private var progressTimer: ProgressBar? = null + private var rectanglesView: FaceRectView? = null + private var faceRectTransformer: FaceRectTransformer? = null + private var frontFotoapparat: Fotoapparat? = null + + lateinit var lytPrepareFaceCapture: ConstraintLayout + lateinit var imgFaceCapture: ImageView + lateinit var txtFaceCapture: TextView + lateinit var txtFaceCaptureWarning: TextView + + private var state: State = State.IDLE + private var timer: CountDownTimer? = null + + private var facingStartTime = 0L + private var hasShakeToLeft = false + private var hasShakeToRight = false + private var hasLookUp = false + private var hasLookNod = false + private var hasZoomIn = false + private var hasZoomOut = false + private var lastEyeClosed = false + private var faceCaptured = 0 + + private var yawThreshold = 0.0f + private var rollThreshold = 0.0f + private var pitchThreshold = 0.0f + private var maxLivenessCount = 0 + private var minimumLivenessRange = 0 + private var maximumLivenessRange = 0 + private var timeoutEachActions = 0 + private var timeoutFaceCapture = 0 + private var hasPostProcess = false + private var hasCoolDown = false + private var postProcessAddress = "" + private var minimumLuminance = 0 + private var maximumLuminance = 0 + + private val mHandler: Handler = object : Handler() { + override fun handleMessage(msg: Message) { + val i: Int = msg.what + if (i == HANDLE_UPDATE_FACE) { + var detectionResult = msg.obj as ArrayList + rectanglesView!!.setHasFace(detectionResult.count() > 0) + } else if(i == HANDLE_TOAST_SHOW) { + val str: String = msg.obj as String + Toast.makeText( + context, + str, + Toast.LENGTH_SHORT + ).show() + } else if(i == HANDLE_SET_TITLE) { + val str: String = msg.obj as String + textView!!.text = str + } else if(i == HANDLE_VIEW_MODE) { + rectanglesView!!.setMode(msg.obj as FaceRectView.DispState) + } else if(i == HANDLE_START_TIMER) { + val timeOut: Int = msg.obj as Int + startCaptureTimer(timeOut) + } else if(i == HANDLE_STOP_TIMER) { + timer?.cancel() + + progressTimer?.progress = timeoutEachActions + txtTimer?.text = "" + timeoutEachActions + "s" + } else if(i == HANDLE_VIBRATOR) { + val v = getSystemService(VIBRATOR_SERVICE) as Vibrator + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + v.vibrate(VibrationEffect.createOneShot(500, VibrationEffect.DEFAULT_AMPLITUDE)) + } else { + //deprecated in API 26 + v.vibrate(500) + } + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_camera) + + context = this + cameraView = findViewById(R.id.camera_view) as CameraView + rectanglesView = findViewById(R.id.rectanglesView) as FaceRectView + textView = findViewById(R.id.textView) as TextView + txtTimer = findViewById(R.id.txtTimer) as TextView + txtWarning = findViewById(R.id.txtWarning) + progressTimer = findViewById(R.id.progressBar) as ProgressBar + lytPrepareFaceCapture = findViewById(R.id.lytPrepareFaceCapture) + imgFaceCapture = findViewById(R.id.imgFaceCapture) + txtFaceCapture = findViewById(R.id.txtFaceCaptureResult) + txtFaceCaptureWarning = findViewById(R.id.txtFaceCaptureWarning) + + var tempActionsIdxs = ArrayList() + for(i in actionsList.indices) + tempActionsIdxs.add(i) + + for(i in actionsList.indices) { + val rand = (Math.random() * 100).toInt() % tempActionsIdxs.size + actionsIdxs.add(tempActionsIdxs.get(rand)) + tempActionsIdxs.removeAt(rand) + } + + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) + yawThreshold = sharedPreferences.getString("valid_yaw_angle", "" + SettingsActivity.DEFAULT_VALID_YAW_ANGLE)!!.toFloat() + rollThreshold = sharedPreferences.getString("valid_roll_angle", "" + SettingsActivity.DEFAULT_VALID_ROLL_ANGLE)!!.toFloat() + pitchThreshold = sharedPreferences.getString("valid_pitch_angle", "" + SettingsActivity.DEFAULT_VALID_PITCH_ANGLE)!!.toFloat() + minimumLivenessRange = sharedPreferences.getString("minimum_range", "" + SettingsActivity.DEFAULT_MINIMUM_RANGE)!!.toInt() + maximumLivenessRange = sharedPreferences.getString("maximum_range", "" + SettingsActivity.DEFAULT_MAXIMUM_RANGE)!!.toInt() + maxLivenessCount = minimumLivenessRange + (Math.random() * (maximumLivenessRange + 1)).toInt() % (maximumLivenessRange - minimumLivenessRange + 1) + timeoutEachActions = sharedPreferences.getString("liveness_timeout", "" + SettingsActivity.DEFAULT_TIMEOUT_EACH_ACTION)!!.toInt() + timeoutFaceCapture = sharedPreferences.getString("face_capture_timeout", "" + SettingsActivity.DEFAULT_TIMEOUT_FACE_CAPTURE)!!.toInt() + hasPostProcess = sharedPreferences.getBoolean("post_process_enable", SettingsActivity.DEFAULT_POST_PROCESS_ENABLE) + hasCoolDown = sharedPreferences.getBoolean("cool_down_enable", SettingsActivity.DEFAULT_COOL_DOWN_ENABLE) + postProcessAddress = sharedPreferences.getString("address_of_api", SettingsActivity.DEFAULT_POST_PROCESS_ADDRESS).toString() + minimumLuminance = sharedPreferences.getString("minimum_range_luminance", "" + SettingsActivity.DEFAULT_MIN_RANGE_LUM)!!.toInt() + maximumLuminance = sharedPreferences.getString("maximum_range_luminance", "" + SettingsActivity.DEFAULT_MAX_RANGE_LUM)!!.toInt() + + progressTimer?.min = 0 + progressTimer?.max = timeoutEachActions + progressTimer?.progress = timeoutEachActions + txtTimer?.text = "" + timeoutEachActions + "s" + + frontFotoapparat = Fotoapparat.with(this) + .into(cameraView!!) + .lensPosition(front()) + .frameProcessor(SampleFrameProcessor()) + .previewResolution { Resolution(PREVIEW_HEIGHT,PREVIEW_WIDTH) } + .build() + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + == PackageManager.PERMISSION_DENIED + ) { + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), 1) + } else { + frontFotoapparat!!.start() + } + } + + + override fun onStop() { + super.onStop() + timer?.cancel() + try { + frontFotoapparat!!.stop() + } catch (e: Exception) { + e.printStackTrace() + } + } + + override fun onResume() { + super.onResume() + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED + ) { + frontFotoapparat!!.start() + } + } + + fun adjustPreview(width: Int, height: Int) : Boolean{ + if(faceRectTransformer == null) { + val frameSize: Size = Size(width, height); + if(cameraView!!.measuredWidth == 0) + return false; + + var displayOrientation: Int = 90; + adjustPreviewViewSize (cameraView!!, + cameraView!!, rectanglesView!!, + Size(frameSize.width, frameSize.height), displayOrientation, 1.0f); + + faceRectTransformer = FaceRectTransformer ( + frameSize.height, frameSize.width, + cameraView!!.getLayoutParams().width, cameraView!!.getLayoutParams().height, + 0, 1, false, + false, + false); + + return true; + } + + return true; + } + + private fun adjustPreviewViewSize( + rgbPreview: View, + previewView: View, + faceRectView: FaceRectView, + previewSize: Size, + displayOrientation: Int, + scale: Float + ): ViewGroup.LayoutParams? { + val layoutParams = previewView.layoutParams + val measuredWidth = previewView.measuredWidth + val measuredHeight = previewView.measuredHeight + layoutParams.width = measuredWidth + layoutParams.height = measuredHeight +// previewView.layoutParams = layoutParams + + faceRectView.layoutParams.width = measuredWidth + faceRectView.layoutParams.height = measuredHeight + return layoutParams + } + + /* access modifiers changed from: private */ /* access modifiers changed from: public */ + private fun sendMessage(w: Int, o: Any?) { + val message = Message() + message.what = w as Int + message.obj = o + mHandler.sendMessage(message) + } + + inner class SampleFrameProcessor : FrameProcessor { + var frThreadQueue: LinkedBlockingQueue? = null + var frExecutor: ExecutorService? = null + init { + frThreadQueue = LinkedBlockingQueue(1) + frExecutor = ThreadPoolExecutor( + 1, 1, 0, TimeUnit.MILLISECONDS, frThreadQueue + ) { r: Runnable? -> + val t = Thread(r) + t.name = "frThread-" + t.id + t + } + } + + override fun invoke(frame: Frame) { + if(state == State.END) + return + + val bitmap = FaceSDK.yuv2Bitmap(frame.image, frame.size.width, frame.size.height, 7) + + val faceDetectionParam = FaceDetectionParam() + faceDetectionParam.check_face_occlusion = true + faceDetectionParam.check_eye_closeness = true + faceDetectionParam.check_mouth_opened = true + val faceResults = FaceSDK.faceDetection(bitmap, faceDetectionParam) + Log.e("TestEngine", "face result count " + faceResults.size) + val faceCount = faceResults?.count() ?: 0 + var face: FaceBox? = null + if (faceResults != null && !faceResults.isEmpty()) { + face = faceResults[0] + } + + if (faceCount == 0) { + if(state != State.IDLE && state != State.LIVENESS_CHECK_COMPLETED) { + state = State.IDLE + endProcess("Liveness check failed") + return + } + } else if (faceCount > 1) { +// endProcess("Please make sure there is only one face on the screen.") +// return + } + + when (state) { + State.IDLE -> { + setTitle("Place your face in center") + setViewMode(FaceRectView.DispState.NO_FACE) + + val faceInRect = isFaceInDetectionRect(face, frame.size.width, frame.size.height) + if(faceInRect == ROI_CHECK_RESULT.ROI_NO_FACE) { + setWarning("No face") + facingStartTime = 0L + } else if(faceInRect == ROI_CHECK_RESULT.ROI_INTERSECTS) { + setWarning("Fit in circle") + facingStartTime = 0L + } else if(faceInRect == ROI_CHECK_RESULT.ROI_SMALL_FACE) { + setWarning("Move closer") + facingStartTime = 0L + } else if(!isFacingCamera(face)) { + setWarning("See front") + facingStartTime = 0L + } else { + if(facingStartTime == 0L) { + facingStartTime = System.currentTimeMillis() + setWarning("") + } else if(System.currentTimeMillis() - facingStartTime >= FACING_CAMERA_KEEP_TIME){ + + postProcess(state.toString()) + gotoNextAction() + } + } + } + State.START_LOOK_UP -> { + setViewMode(FaceRectView.DispState.ROUND_NORMAL) + val faceInRect = isFaceInDetectionRect(face, frame.size.width, frame.size.height) + if(faceInRect == ROI_CHECK_RESULT.ROI_NO_FACE) { + setTitle( "Place your face in center") + setWarning("No face") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_INTERSECTS) { + setTitle( "Place your face in center") + setWarning("Fit in circle") + } else if(!isFacingCamera(face)) { + setTitle( "Place your face in center") + setWarning("See front") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_SMALL_FACE) { + setTitle( "Place your face in center") + setWarning("Move closer") + } else { + setTitle("Look Up") + setWarning("") + startTimer(timeoutEachActions) + state = State.LOOK_UP + hasLookUp = false + } + } + State.LOOK_UP -> { + val pitch = face?.pitch ?: 0f + val thresholdLookup = -pitchThreshold + if (pitch < thresholdLookup && !hasLookUp) { + hasLookUp = true + } + + if (hasLookUp) { + postProcess(state.toString()) + gotoNextAction() + } + } + State.START_NOD -> { + setViewMode(FaceRectView.DispState.ROUND_NORMAL) + val faceInRect = isFaceInDetectionRect(face, frame.size.width, frame.size.height) + if(faceInRect == ROI_CHECK_RESULT.ROI_NO_FACE) { + setTitle( "Place your face in center") + setWarning("No face") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_INTERSECTS) { + setTitle( "Place your face in center") + setWarning("Fit in circle") + } else if(!isFacingCamera(face)) { + setTitle( "Place your face in center") + setWarning("See front") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_SMALL_FACE) { + setTitle( "Place your face in center") + setWarning("Move closer") + } else { + setTitle( "Look Down") + setWarning("") + startTimer(timeoutEachActions) + state = State.NOD + hasLookNod = false + } + } + State.NOD -> { + val pitch = face?.pitch ?: 0f + val thresholdLookNod = pitchThreshold + if (pitch > thresholdLookNod && !hasLookNod) { + hasLookNod = true + } + if (hasLookNod) { + postProcess(state.toString()) + gotoNextAction() + } + } + State.START_LOOK_UP_NOD -> { + setViewMode(FaceRectView.DispState.ROUND_NORMAL) + val faceInRect = isFaceInDetectionRect(face, frame.size.width, frame.size.height) + if(faceInRect == ROI_CHECK_RESULT.ROI_NO_FACE) { + setTitle( "Place your face in center") + setWarning("No face") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_INTERSECTS) { + setTitle( "Place your face in center") + setWarning("Fit in circle") + } else if(!isFacingCamera(face)) { + setTitle( "Place your face in center") + setWarning("See front") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_SMALL_FACE) { + setTitle( "Place your face in center") + setWarning("Move closer") + } else { + setTitle( "Look Up => Down") + setWarning("") + startTimer(timeoutEachActions) + state = State.LOOK_UP_NOD + hasLookUp = false + hasLookNod = false + } + } + State.LOOK_UP_NOD -> { + val pitch = face?.pitch ?: 0f + val thresholdLookup = -pitchThreshold + if (pitch < thresholdLookup && !hasLookUp && !hasLookNod) { + hasLookUp = true + postProcess(state.toString() + "1") + } + + val thresholdLookNod = 12f + if (pitch > thresholdLookNod && hasLookUp && !hasLookNod) { + hasLookNod = true + postProcess(state.toString() + "2") + } + + if (hasLookUp && hasLookNod) { + gotoNextAction() + } + } + State.START_NOD_LOOK_UP -> { + setViewMode(FaceRectView.DispState.ROUND_NORMAL) + val faceInRect = isFaceInDetectionRect(face, frame.size.width, frame.size.height) + if(faceInRect == ROI_CHECK_RESULT.ROI_NO_FACE) { + setTitle( "Place your face in center") + setWarning("No face") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_INTERSECTS) { + setTitle( "Place your face in center") + setWarning("Fit in circle") + } else if(!isFacingCamera(face)) { + setTitle( "Place your face in center") + setWarning("See front") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_SMALL_FACE) { + setTitle( "Place your face in center") + setWarning("Move closer") + } else { + setTitle( "Look Down => Up") + setWarning("") + startTimer(timeoutEachActions) + state = State.NOD_LOOK_UP + hasLookUp = false + hasLookNod = false + } + } + State.NOD_LOOK_UP -> { + val pitch = face?.pitch ?: 0f + val thresholdLookNod = pitchThreshold + if (pitch > thresholdLookNod && !hasLookUp && !hasLookNod) { + hasLookNod = true + postProcess(state.toString() + "1") + } + + val thresholdLookup = -pitchThreshold + if (pitch < thresholdLookup && !hasLookUp && hasLookNod) { + hasLookUp = true + postProcess(state.toString() + "2") + } + + if (hasLookUp && hasLookNod) { + gotoNextAction() + } + } + State.START_ZOOM_IN -> { + setTitle("Zoom In") + setViewMode(FaceRectView.DispState.ROUND_ZOOM_IN) + startTimer(timeoutEachActions) + state = State.ZOOM_IN + } + State.ZOOM_IN -> { + val faceInRect = isFaceInDetectionRect(face, frame.size.width, frame.size.height) + if(faceInRect == ROI_CHECK_RESULT.ROI_NO_FACE) { + setWarning("No face") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_INTERSECTS) { + setWarning("Fit in circle") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_SMALL_FACE) { + setWarning("Move closer") + } else { + postProcess(state.toString()) + gotoNextAction() + } + } + State.START_ZOOM_IN_OUT -> { + setTitle("Zoom In => Out") + setViewMode(FaceRectView.DispState.ROUND_ZOOM_IN) + startTimer(timeoutEachActions) + state = State.ZOOM_IN_OUT + hasZoomIn = false + hasZoomOut = false + } + State.ZOOM_IN_OUT -> { + val faceInRect = isFaceInDetectionRect(face, frame.size.width, frame.size.height) + if(faceInRect == ROI_CHECK_RESULT.ROI_NO_FACE) { + setWarning("No face") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_INTERSECTS) { + setWarning("Fit in circle") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_SMALL_FACE) { + setWarning("Move closer") + } else { + + if(hasZoomIn == false && hasZoomOut == false) { + hasZoomIn = true + postProcess(state.toString() + "1") + setViewMode(FaceRectView.DispState.ROUND_ZOOM_OUT) + } + else if(hasZoomIn == true && hasZoomOut == false) { + postProcess(state.toString() + "2") + gotoNextAction() + } + } + } + State.START_ZOOM_OUT -> { + setTitle("Zoom Out") + setViewMode(FaceRectView.DispState.ROUND_ZOOM_OUT) + startTimer(timeoutEachActions) + state = State.ZOOM_OUT + } + State.ZOOM_OUT -> { + val faceInRect = isFaceInDetectionRect(face, frame.size.width, frame.size.height) + if(faceInRect == ROI_CHECK_RESULT.ROI_NO_FACE) { + setWarning("No face") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_INTERSECTS) { + setWarning("Fit in circle") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_SMALL_FACE) { + setWarning("Move closer") + } else { + postProcess(state.toString()) + gotoNextAction() + } + } + State.START_ZOOM_OUT_IN -> { + setTitle("Zoom Out => In") + setViewMode(FaceRectView.DispState.ROUND_ZOOM_OUT) + startTimer(timeoutEachActions) + state = State.ZOOM_OUT_IN + hasZoomIn = false + hasZoomOut = false + } + State.ZOOM_OUT_IN -> { + val faceInRect = isFaceInDetectionRect(face, frame.size.width, frame.size.height) + if(faceInRect == ROI_CHECK_RESULT.ROI_NO_FACE) { + setWarning("No face") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_INTERSECTS) { + setWarning("Fit in circle") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_SMALL_FACE) { + setWarning("Move closer") + } else { + if(hasZoomIn == false && hasZoomOut == false) { + hasZoomOut = true + setViewMode(FaceRectView.DispState.ROUND_ZOOM_IN) + postProcess(state.toString() + "1") + } else if(hasZoomIn == false && hasZoomOut == true) { + postProcess(state.toString() + "2") + gotoNextAction() + } + } + } + State.START_MOUTH -> { + setViewMode(FaceRectView.DispState.ROUND_NORMAL) + val faceInRect = isFaceInDetectionRect(face, frame.size.width, frame.size.height) + if(faceInRect == ROI_CHECK_RESULT.ROI_NO_FACE) { + setTitle( "Place your face in center") + setWarning("No face") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_INTERSECTS) { + setTitle( "Place your face in center") + setWarning("Fit in circle") + } else if(!isFacingCamera(face)) { + setTitle( "Place your face in center") + setWarning("See front") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_SMALL_FACE) { + setTitle( "Place your face in center") + setWarning("Move closer") + } else { + setTitle( "Open your mouth") + setWarning("") + + startTimer(timeoutEachActions) + state = State.MOUTH + } + } + State.MOUTH -> { + if (isMouthOpened(face)) { + postProcess(state.toString()) + gotoNextAction() + } + } + State.START_EYE_BLINK -> { + setViewMode(FaceRectView.DispState.ROUND_NORMAL) + val faceInRect = isFaceInDetectionRect(face, frame.size.width, frame.size.height) + if(faceInRect == ROI_CHECK_RESULT.ROI_NO_FACE) { + setTitle( "Place your face in center") + setWarning("No face") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_INTERSECTS) { + setTitle( "Place your face in center") + setWarning("Fit in circle") + } else if(!isFacingCamera(face)) { + setTitle( "Place your face in center") + setWarning("See front") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_SMALL_FACE) { + setTitle( "Place your face in center") + setWarning("Move closer") + } else { + setTitle( "Blink your eyes") + setWarning("") + + if(face!!.left_eye_closed < 0.5 && face!!.right_eye_closed < 0.5) { + startTimer(timeoutEachActions) + state = State.EYE_BLINK + } + } + } + State.EYE_BLINK -> { + if(isEyeBlinking(face)) { + postProcess(state.toString()) + gotoNextAction() + } + } + State.START_TURN_LEFT -> { + setViewMode(FaceRectView.DispState.ROUND_NORMAL) + val faceInRect = isFaceInDetectionRect(face, frame.size.width, frame.size.height) + if(faceInRect == ROI_CHECK_RESULT.ROI_NO_FACE) { + setTitle( "Place your face in center") + setWarning("No face") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_INTERSECTS) { + setTitle( "Place your face in center") + setWarning("Fit in circle") + } else if(!isFacingCamera(face)) { + setTitle( "Place your face in center") + setWarning("See front") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_SMALL_FACE) { + setTitle( "Place your face in center") + setWarning("Move closer") + } else { + setTitle( "Look Left") + setWarning("") + + startTimer(timeoutEachActions) + state = State.TURN_LEFT + hasShakeToLeft = false + } + } + State.TURN_LEFT -> { + val yaw = face?.yaw ?: 0f + val thresholdLeft = yawThreshold + if (yaw > thresholdLeft && !hasShakeToLeft) { + hasShakeToLeft = true + } + if (hasShakeToLeft) { + postProcess(state.toString()) + gotoNextAction() + } + } + State.START_TURN_RIGHT -> { + setViewMode(FaceRectView.DispState.ROUND_NORMAL) + val faceInRect = isFaceInDetectionRect(face, frame.size.width, frame.size.height) + if(faceInRect == ROI_CHECK_RESULT.ROI_NO_FACE) { + setTitle( "Place your face in center") + setWarning("No face") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_INTERSECTS) { + setTitle( "Place your face in center") + setWarning("Fit in circle") + } else if(!isFacingCamera(face)) { + setTitle( "Place your face in center") + setWarning("See front") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_SMALL_FACE) { + setTitle( "Place your face in center") + setWarning("Move closer") + } else { + setTitle( "Look right") + setWarning("") + + startTimer(timeoutEachActions) + state = State.TURN_RIGHT + hasShakeToRight = false + } + } + State.TURN_RIGHT -> { + val yaw = face?.yaw ?: 0f + val thresholdRight = -yawThreshold + if (yaw < thresholdRight && !hasShakeToRight) { + hasShakeToRight = true + } + if (hasShakeToRight) { + postProcess(state.toString()) + gotoNextAction() + } + } + State.START_TURN_LEFT_RIGHT -> { + setViewMode(FaceRectView.DispState.ROUND_NORMAL) + val faceInRect = isFaceInDetectionRect(face, frame.size.width, frame.size.height) + if(faceInRect == ROI_CHECK_RESULT.ROI_NO_FACE) { + setTitle( "Place your face in center") + setWarning("No face") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_INTERSECTS) { + setTitle( "Place your face in center") + setWarning("Fit in circle") + } else if(!isFacingCamera(face)) { + setTitle( "Place your face in center") + setWarning("See front") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_SMALL_FACE) { + setTitle( "Place your face in center") + setWarning("Move closer") + } else { + setTitle( "Look Left => Right") + setWarning("") + + startTimer(timeoutEachActions) + state = State.TURN_LEFT_RIGHT + hasShakeToLeft = false + hasShakeToRight = false + + } + } + State.TURN_LEFT_RIGHT -> { + val yaw = face?.yaw ?: 0f + val thresholdLeft = yawThreshold + if (yaw > thresholdLeft && !hasShakeToLeft && !hasShakeToRight) { + hasShakeToLeft = true + postProcess(state.toString() + "1") + } + + val thresholdRight = -yawThreshold + if (yaw < thresholdRight && hasShakeToLeft && !hasShakeToRight) { + hasShakeToRight = true + postProcess(state.toString() + "2") + } + + if (hasShakeToLeft && hasShakeToRight) { + gotoNextAction() + } + } + State.START_TURN_RIGHT_LEFT -> { + setViewMode(FaceRectView.DispState.ROUND_NORMAL) + val faceInRect = isFaceInDetectionRect(face, frame.size.width, frame.size.height) + if(faceInRect == ROI_CHECK_RESULT.ROI_NO_FACE) { + setTitle( "Place your face in center") + setWarning("No face") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_INTERSECTS) { + setTitle( "Place your face in center") + setWarning("Fit in circle") + } else if(!isFacingCamera(face)) { + setTitle( "Place your face in center") + setWarning("See front") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_SMALL_FACE) { + setTitle( "Place your face in center") + setWarning("Move closer") + } else { + setTitle( "Look Right => Left") + setWarning("") + + startTimer(timeoutEachActions) + state = State.TURN_RIGHT_LEFT + hasShakeToLeft = false + hasShakeToRight = false + + } + } + State.TURN_RIGHT_LEFT -> { + val yaw = face?.yaw ?: 0f + val thresholdRight = -yawThreshold + if (yaw < thresholdRight && !hasShakeToLeft && !hasShakeToRight) { + hasShakeToRight = true + } + + val thresholdLeft = yawThreshold + if (yaw > thresholdLeft && !hasShakeToLeft && hasShakeToRight) { + hasShakeToLeft = true + postProcess(state.toString() + "1") + } + + if (hasShakeToLeft && hasShakeToRight) { + postProcess(state.toString() + "2") + gotoNextAction() + } + } + State.LIVENESS_CHECK_COMPLETED -> { + + setViewMode(FaceRectView.DispState.ROUND_NORMAL) + setTitle( "Face Capture") + if(faceCaptured == 0) { + faceCaptured = 1 + if(hasCoolDown) { + startTimer(timeoutFaceCapture) + } + } + + val luminance = calculateLuminance(frame) + val faceInRect = isFaceInDetectionRect(face, frame.size.width, frame.size.height) + if(faceInRect == ROI_CHECK_RESULT.ROI_NO_FACE) { + setWarning("No face") + } else if(luminance < minimumLuminance) { + setWarning("Low luminance") + } else if(luminance > maximumLuminance) { + setWarning("High luminance") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_INTERSECTS) { + setWarning("Fit in circle") + } else if(!isFacingCamera(face)) { + setWarning("See front") + } else if(faceInRect == ROI_CHECK_RESULT.ROI_SMALL_FACE) { + setWarning("Move closer") + } else if(isMouthOpened(face)) { + setWarning("Close mouth") + } else if(face!!.left_eye_closed > 0.5 || face!!.right_eye_closed > 0.5) { + setWarning("Do not blink") + } else { + setWarning("") + if(hasCoolDown == false) { + faceCaptured = 2 + } + } + + if(faceCaptured == 2) { + faceCaptured = 3 + faceCapture(frame) + } + } + State.END -> { + + } + } + + if(adjustPreview(frame.size.width, frame.size.height)) + sendMessage(HANDLE_UPDATE_FACE, faceResults) + + } + } + + private fun startCaptureTimer(timeOut: Int) { + + timer?.cancel() + timer = object: CountDownTimer((timeOut * 1000).toLong() + 1000L, 1000) { + override fun onTick(millisUntilFinished: Long) { + runOnUiThread { + txtTimer?.text = (millisUntilFinished / 1000).toString() + "s" + progressTimer?.progress = (millisUntilFinished / 1000).toInt() + } + } + + override fun onFinish() { + if(state == State.LIVENESS_CHECK_COMPLETED) { + faceCaptured = 2 + } else { + endProcess("Liveness check timeout") + } + } + } + timer?.start() + } + + private fun isFaceInDetectionRect(face: FaceBox?, frameWidth: Int, frameHeight: Int): ROI_CHECK_RESULT { + face ?: return ROI_CHECK_RESULT.ROI_NO_FACE + val maxSize = Math.max(frameWidth, frameHeight) + val minSize = Math.min(frameWidth, frameHeight) + var sizeRate = 0.45f + var interRate = 0.1f + + val viewWidth = rectanglesView!!.width + val viewHeight = rectanglesView!!.height + val minView = Math.min(viewWidth, viewHeight) + val maxView = Math.max(viewWidth, viewHeight) + val ratioView = minView / maxView.toFloat() + val ratioFrame = minSize / maxSize.toFloat() + + var cropRect = Rect() + if(state != State.ZOOM_IN && state != State.ZOOM_OUT && state != State.ZOOM_IN_OUT && state != State.ZOOM_OUT_IN) { + val margin = minView / 6 + val rectHeight = (minView - 2 * margin) * 4 / 3 + cropRect = Rect(margin.toInt(), + ((maxView - rectHeight) / 2).toInt(), + (minView - margin).toInt(), + ((maxView - rectHeight) / 2 + rectHeight).toInt() + ) + } else if(state == State.ZOOM_OUT || (state == State.ZOOM_IN_OUT && hasZoomIn == true) || (state == State.ZOOM_OUT_IN && hasZoomOut == false)) { + val margin = minView / 4 + val rectHeight = (minView - 2 * margin) * 4 / 3 + cropRect = Rect(margin.toInt(), + ((maxView - rectHeight) / 2).toInt(), + (minView - margin).toInt(), + ((maxView - rectHeight) / 2 + rectHeight).toInt() + ) + interRate = 0.01f + sizeRate = 0.50f + } else if(state == State.ZOOM_IN || (state == State.ZOOM_IN_OUT && hasZoomIn == false) || (state == State.ZOOM_OUT_IN && hasZoomOut == true)) { + val margin = minView / 15 + val rectHeight = minView * 7 / 5 + cropRect = Rect(margin.toInt(), + ((maxView - rectHeight) / 2).toInt(), + (minView - margin).toInt(), + ((maxView - rectHeight) / 2 + rectHeight).toInt() + ) + sizeRate = 0.55f + } + + var frameCropRect = Rect() + if(ratioView < ratioFrame) { + var dx = ((maxView * ratioFrame) - minView) / 2 + var dy = 0f + var ratio = maxSize / maxView.toFloat() + + val x1 = (cropRect.left + dx) * ratio + val y1 = (cropRect.top + dy) * ratio + val x2 = (cropRect.right + dx) * ratio + val y2 = (cropRect.bottom + dy) * ratio + frameCropRect = Rect(x1.toInt(), y1.toInt(), x2.toInt(), y2.toInt()) + } else { + var dx = 0f + var dy = ((minView / ratioFrame) - maxView) / 2 + var ratio = maxSize / maxView.toFloat() + + val x1 = (cropRect.left + dx) * ratio + val y1 = (cropRect.top + dy) * ratio + val x2 = (cropRect.right + dx) * ratio + val y2 = (cropRect.bottom + dy) * ratio + frameCropRect = Rect(x1.toInt(), y1.toInt(), x2.toInt(), y2.toInt()) + } + + var faceLeft = Float.MAX_VALUE + var faceRight = 0f + var faceBottom = 0f + for(i in 0..67) { + faceLeft = Math.min(faceLeft, face.landmarks_68[i * 2]) + faceRight = Math.max(faceRight, face.landmarks_68[i * 2]) + faceBottom = Math.max(faceBottom, face.landmarks_68[i * 2 + 1]) + } + + + val centerY = (face.y2 + face.y1) / 2 + val topY = centerY - (face.y2 - face.y1) * 2 / 3 + + var interX = Math.max(0f, frameCropRect.left.toFloat() - faceLeft) + Math.max(0f, faceRight - frameCropRect.right.toFloat()) + var interY = Math.max(0f, frameCropRect.top.toFloat() - topY) + Math.max(0f, faceBottom - frameCropRect.bottom.toFloat()) + + if(interX / frameCropRect.width().toFloat() > interRate || interY / frameCropRect.height().toFloat() > interRate) { + return ROI_CHECK_RESULT.ROI_INTERSECTS + } + + if((face.y2 - face.y1) * (face.x2 - face.x1) < frameCropRect.width() * frameCropRect.height() * sizeRate) { + return ROI_CHECK_RESULT.ROI_SMALL_FACE + } + + return ROI_CHECK_RESULT.ROI_FACE_OK + } + + private fun isFacingCamera(face: FaceBox?): Boolean { + face ?: return false + return face.roll < rollThreshold && face.roll > -rollThreshold + && face.yaw < yawThreshold && face.yaw > -yawThreshold + && face.pitch < pitchThreshold && face.pitch > -pitchThreshold + } + + private fun isEyeBlinking(face:FaceBox?): Boolean { + face ?: return false + + if(lastEyeClosed == false) { + if(face.left_eye_closed > 0.5 && face.right_eye_closed > 0.5) { + lastEyeClosed = true + return false + } + } else { + if(face.left_eye_closed < 0.5 && face.right_eye_closed < 0.5) { + return true + } + } + + return false + } + + private fun isMouthOpened(face: FaceBox?): Boolean { + face ?: return false + return face.mouth_opened > 0.5; + } + + private fun lengthSquare(a: PointF, b: PointF): kotlin.Float { + val x = a.x - b.x + val y = a.y - b.y + return x * x + y * y + } + + private fun setTitle(msg:String) { + sendMessage(HANDLE_SET_TITLE, msg) + } + + private fun setViewMode(mode: FaceRectView.DispState) { + sendMessage(HANDLE_VIEW_MODE, mode) + } + + private fun startTimer(timeout: Int) { + sendMessage(HANDLE_START_TIMER, timeout) + } + + private fun setWarning(msg: String) { + warningMessage = msg + runOnUiThread { txtWarning.text = warningMessage } + } + + private fun endProcess(msg: String) { + state = State.END + timer?.cancel() + sendMessage(HANDLE_TOAST_SHOW, msg) + finish() + } + + private fun gotoNextAction() { + + setWarning("") + sendMessage(HANDLE_STOP_TIMER, 0) + sendMessage(HANDLE_VIBRATOR, 0) + if(state == State.LIVENESS_CHECK_COMPLETED) { + lytPrepareFaceCapture.visibility = View.VISIBLE + + val savePath = filesDir.path + "/capture.jpg" + val bitmapCapture = Utils.getCorrectlyOrientedImage(context, savePath) + imgFaceCapture.setImageBitmap(bitmapCapture) + + val faceDetectionParam = FaceDetectionParam() + faceDetectionParam.check_face_occlusion = true + faceDetectionParam.check_eye_closeness = true + faceDetectionParam.check_mouth_opened = true + val faceResults = FaceSDK.faceDetection(bitmapCapture, faceDetectionParam) + var face: FaceBox? = null + if (faceResults != null && !faceResults.isEmpty()) { + face = faceResults[0] + } + + val luminance = calcLuminanceFromBitmap(bitmapCapture) + val faceInRect = isFaceInDetectionRect(face, bitmapCapture.width, bitmapCapture.height) + if(faceInRect == ROI_CHECK_RESULT.ROI_NO_FACE) { + txtFaceCapture.text = "Face capture failed!" + txtFaceCaptureWarning.text = "No face" + } else if(luminance < minimumLuminance) { + txtFaceCapture.text = "Face capture failed!" + txtFaceCaptureWarning.text = "Low luminance" + } else if(luminance > maximumLuminance) { + txtFaceCapture.text = "Face capture failed!" + txtFaceCaptureWarning.text = "High luminance" + } else if(faceInRect == ROI_CHECK_RESULT.ROI_INTERSECTS) { + txtFaceCapture.text = "Face capture failed!" + txtFaceCaptureWarning.text = "Fit in circle" + } else if(!isFacingCamera(face)) { + txtFaceCapture.text = "Face capture failed!" + txtFaceCaptureWarning.text = "See front" + } else if(faceInRect == ROI_CHECK_RESULT.ROI_SMALL_FACE) { + txtFaceCapture.text = "Face capture failed!" + txtFaceCaptureWarning.text = "Move closer" + } else if(isMouthOpened(face)) { + txtFaceCapture.text = "Face capture failed!" + txtFaceCaptureWarning.text = "Close mouth" + } else if(face!!.left_eye_closed > 0.5 || face!!.right_eye_closed > 0.5) { + txtFaceCapture.text = "Face capture failed!" + txtFaceCaptureWarning.text = "Do not blink" + } else { + txtFaceCapture.text = "Face capture succeed!" + txtFaceCaptureWarning.text = "" + + if(hasPostProcess) { + val thread = thread(start = true) { + // Code to run in the new thread + FileUploadRunnable(savePath).run() + } + } + } + state = State.END + } else if(currentActionIdx >= maxLivenessCount) { + state = State.LIVENESS_CHECK_COMPLETED + } else { + state = actionsList[actionsIdxs[currentActionIdx]] + } + currentActionIdx ++ + + if(state == State.LIVENESS_CHECK_COMPLETED) { + runOnUiThread { + Toast.makeText( + context, + "Liveness Check Succeed", + Toast.LENGTH_SHORT + ).show() + } + } + } + + private fun Bitmap.flip(horizontal: Boolean, vertical: Boolean): Bitmap { + val matrix = Matrix() + matrix.preScale((if (horizontal) -1 else 1).toFloat(), (if (vertical) -1 else 1).toFloat()) + return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true) + } + + private fun Bitmap.rotate(degrees: Float): Bitmap { + val matrix = Matrix() + matrix.postRotate(degrees) + val scaledBitmap = Bitmap.createScaledBitmap(this, width, height, true) + return Bitmap.createBitmap( + scaledBitmap, + 0, + 0, + scaledBitmap.width, + scaledBitmap.height, + matrix, + true + ) + } + + private fun postProcess(saveName: String) { + if(hasPostProcess == false) { + return + } + + val savePath = filesDir.path + "/" + saveName + val saveFile = File(savePath) + if(!saveFile.exists()) { + saveFile.createNewFile() + } + + frontFotoapparat!!.takePicture().saveToFile(saveFile).whenAvailable { + val thread = thread(start = true) { + // Code to run in the new thread + FileUploadRunnable(savePath).run() + } + } + } + + private fun faceCapture(frame: Frame) { + val savePath = filesDir.path + "/capture.jpg" + val saveFile = File(savePath) + if(!saveFile.exists()) { + saveFile.createNewFile() + } + + YuvImage( + frame.image, + ImageFormat.NV21, + frame.size.width, + frame.size.height, + null + ).let { yuvImage -> + ByteArrayOutputStream().use { output -> + yuvImage.compressToJpeg( + Rect(0, 0, frame.size.width, frame.size.height), + 100, + output + ) + output.toByteArray().apply { + BitmapFactory.decodeByteArray(this, 0, size)?.let { bitmap -> + + val fixedBitmap = bitmap.rotate(90f).flip( + false, true + ) + + try { + val fos = FileOutputStream(saveFile) + fixedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos) + fos.flush() + fos.close() + + runOnUiThread { + frontFotoapparat!!.stop() + gotoNextAction() + if(hasPostProcess) { + val thread = thread(start = true) { + // Code to run in the new thread + FileUploadRunnable(savePath).run() + } + } + } + } catch (e: IOException) { + e.printStackTrace() + } + } + } + } + } + } + + inner class FileUploadRunnable(saveName_: String) : Runnable { + val saveName: String + + init { + saveName = saveName_ + } + + override fun run() { +// try { +// // Load the certificate file +// val inputStream: InputStream = resources.openRawResource(R.raw.mycert) +// val certificateFactory = CertificateFactory.getInstance("X.509") +// val serverCert = certificateFactory.generateCertificate(inputStream) as X509Certificate +// +// val trustStore = KeyStore.getInstance(KeyStore.getDefaultType()) +// trustStore.load(null, null) +// trustStore.setCertificateEntry("server", serverCert) +// +// val trustManagerFactory = TrustManagerFactory.getInstance( +// TrustManagerFactory.getDefaultAlgorithm() +// ) +// trustManagerFactory.init(trustStore) +// +// val sslContext = SSLContext.getInstance("TLS") +// sslContext.init(null, trustManagerFactory.trustManagers, SecureRandom()) +// +// val client = OkHttpClient.Builder() +// .addInterceptor(HttpLoggingInterceptor().apply { +// level = HttpLoggingInterceptor.Level.BODY +// }) +// .sslSocketFactory(sslContext.socketFactory, trustManagerFactory.trustManagers[0] as X509TrustManager) +// .build() +// +// val file = File(saveName) +// val requestBody: RequestBody = MultipartBody.Builder() +// .setType(MultipartBody.FORM) +// .addFormDataPart( +// "image", +// file.name, +// RequestBody.create("multipart/form-data".toMediaTypeOrNull(), file) +// ) +// .build() +// +// val request: Request = Builder() +// .url(postProcessAddress) +// .post(requestBody) +// .build() +// +// val response = client.newCall(request).execute() +// Log.e("TestEngine", "response: " + response) +// } catch(e:Exception) { +// e.printStackTrace() +// } + } + } + + fun calculateLuminance(frame: Frame): Double { + // Get the Y (luminance) plane + val yBuffer = frame.image + val yRowStride = frame.size.width + val yPixelStride = 1 + + val width = frame.size.width + val height = frame.size.height + + var totalLuminance = 0.0 + var pixelCount = 0 + + for (row in 0 until height) { + for (col in 0 until width step yPixelStride) { + val index = row * yRowStride + col + val yValue = yBuffer.get(index).toInt() and 0xFF // Convert byte to unsigned int + totalLuminance += yValue + pixelCount++ + } + } + + // Calculate the average luminance + return if (pixelCount > 0) totalLuminance / pixelCount else 0.0 + } + + fun calcLuminanceFromBitmap(bitmap: Bitmap): Int { + val width = bitmap.width + val height = bitmap.height + + // Convert the Bitmap to ARGB pixels + val pixels = IntArray(width * height) + bitmap.getPixels(pixels, 0, width, 0, 0, width, height) + + var sum = 0.0 + + // Iterate through each pixel and calculate luminance + for (pixel in pixels) { + val R = (pixel shr 16) and 0xFF // Extract red channel + val G = (pixel shr 8) and 0xFF // Extract green channel + val B = pixel and 0xFF // Extract blue channel + + // Calculate Y (luminance) using the same formula + val Y = ((66 * R + 129 * G + 25 * B + 128) shr 8) + 16 + sum += Y + } + + // Return the average luminance + return (sum / (width * height)).toInt() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kbyai/faceattribute/CaptureActivityKt.kt b/app/src/main/java/com/kbyai/faceattribute/CaptureActivityKt.kt new file mode 100644 index 0000000..68ef590 --- /dev/null +++ b/app/src/main/java/com/kbyai/faceattribute/CaptureActivityKt.kt @@ -0,0 +1,318 @@ +package com.kbyai.faceattribute + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.os.Bundle +import android.util.Size +import android.view.View +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.camera.core.CameraSelector +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.preference.PreferenceManager + +import com.kbyai.facesdk.FaceBox +import com.kbyai.facesdk.FaceDetectionParam +import com.kbyai.facesdk.FaceSDK +import io.fotoapparat.Fotoapparat +import io.fotoapparat.parameter.Resolution +import io.fotoapparat.preview.Frame +import io.fotoapparat.preview.FrameProcessor +import io.fotoapparat.selector.front +import io.fotoapparat.view.CameraView +import java.util.Random +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +class CaptureActivityKt : AppCompatActivity(), CaptureView.ViewModeChanged { + + val TAG = CaptureActivityKt::class.java.simpleName + val PREVIEW_WIDTH = 720 + val PREVIEW_HEIGHT = 1280 + + private lateinit var fotoapparat: Fotoapparat + private lateinit var context: Context + + private lateinit var cameraView: CameraView + + private lateinit var captureView: CaptureView + + private lateinit var warningTxt: TextView + + private lateinit var livenessTxt: TextView + + private lateinit var qualityTxt: TextView + + private lateinit var luminaceTxt: TextView + + private lateinit var lytCaptureResult: ConstraintLayout + + private var capturedBitmap: Bitmap? = null + + private var capturedFace: FaceBox? = null + + private var yawThreshold = 0.0f + private var rollThreshold = 0.0f + private var pitchThreshold = 0.0f + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_capture_kt) + + context = this + cameraView = findViewById(R.id.preview) + captureView = findViewById(R.id.captureView) + warningTxt = findViewById(R.id.txtWarning) + livenessTxt = findViewById(R.id.txtLiveness) + qualityTxt = findViewById(R.id.txtQuality) + luminaceTxt = findViewById(R.id.txtLuminance) + lytCaptureResult = findViewById(R.id.lytCaptureResult) + + captureView.setViewModeInterface(this) + captureView.setViewMode(CaptureView.VIEW_MODE.NO_FACE_PREPARE) + + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) + yawThreshold = sharedPreferences.getString("valid_yaw_angle", "" + SettingsActivity.DEFAULT_VALID_YAW_ANGLE)!!.toFloat() + rollThreshold = sharedPreferences.getString("valid_roll_angle", "" + SettingsActivity.DEFAULT_VALID_ROLL_ANGLE)!!.toFloat() + pitchThreshold = sharedPreferences.getString("valid_pitch_angle", "" + SettingsActivity.DEFAULT_VALID_PITCH_ANGLE)!!.toFloat() + + findViewById(R.id.buttonEnroll).setOnClickListener { + val faceImage = Utils.cropFace(capturedBitmap, capturedFace) + val templates = FaceSDK.templateExtraction(capturedBitmap, capturedFace) + +// val dbManager = DBManager(context) +// val min = 10000 +// val max = 20000 +// val random = Random().nextInt((max - min) + 1) + min +// +// dbManager.insertPerson("Person$random", faceImage, templates) + Toast.makeText(context, getString(R.string.person_enrolled), Toast.LENGTH_SHORT).show() + finish() + } + + fotoapparat = Fotoapparat.with(this) + .into(cameraView) + .lensPosition(front()) + .frameProcessor(FaceFrameProcessor()) + .previewResolution { Resolution(PREVIEW_HEIGHT,PREVIEW_WIDTH) } + .build() + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + == PackageManager.PERMISSION_DENIED + ) { + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), 1) + } else { + fotoapparat.start() + } + } + + override fun onResume() { + super.onResume() + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED + ) { + fotoapparat.start() + } + } + + override fun onPause() { + super.onPause() + fotoapparat.stop() + captureView.setFaceBoxes(null) + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == 1) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED + ) { + fotoapparat.start() + } + } + } + + override fun view5_finished() { + val param = FaceDetectionParam() + param.check_liveness = true + param.check_liveness_level = 0 + + val faceBoxes = FaceSDK.faceDetection(capturedBitmap, param) + if (faceBoxes != null && faceBoxes.size > 0) { + if (faceBoxes[0].liveness > 0.7) { + val msg = String.format("Liveness: Real, score = %.03f", faceBoxes[0].liveness) + livenessTxt.text = msg + } else { + val msg = String.format("Liveness: Spoof, score = %.03f", faceBoxes[0].liveness) + livenessTxt.text = msg + } + } + + if (capturedFace!!.face_quality < 0.5f) { + val msg = String.format("Quality: Low, score = %.03f", capturedFace!!.face_quality) + qualityTxt.text = msg + } else if (capturedFace!!.face_quality < 0.75f) { + val msg = String.format("Quality: Medium, score = %.03f", capturedFace!!.face_quality) + qualityTxt.text = msg + } else { + val msg = String.format("Quality: High, score = %.03f", capturedFace!!.face_quality) + qualityTxt.text = msg + } + + val msg = String.format("Luminance: %.03f", capturedFace!!.face_luminance) + luminaceTxt.text = msg + + lytCaptureResult.visibility = View.VISIBLE + } + + fun checkFace(faceBoxes: List?, context: Context?): FACE_CAPTURE_STATE { + if (faceBoxes == null || faceBoxes.size == 0) return FACE_CAPTURE_STATE.NO_FACE + + if (faceBoxes.size > 1) { + return FACE_CAPTURE_STATE.MULTIPLE_FACES + } + + val faceBox = faceBoxes[0] + var faceLeft = Float.MAX_VALUE + var faceRight = 0f + var faceBottom = 0f + for (i in 0..67) { + faceLeft = min(faceLeft.toDouble(), faceBox.landmarks_68[i * 2].toDouble()).toFloat() + faceRight = + max(faceRight.toDouble(), faceBox.landmarks_68[i * 2].toDouble()).toFloat() + faceBottom = + max(faceBottom.toDouble(), faceBox.landmarks_68[i * 2 + 1].toDouble()).toFloat() + } + + val sizeRate = 0.30f + val interRate = 0.03f + val frameSize = Size(PREVIEW_WIDTH, PREVIEW_HEIGHT) + val roiRect = CaptureView.getROIRect(frameSize) + val centerY = ((faceBox.y2 + faceBox.y1) / 2).toFloat() + val topY = centerY - (faceBox.y2 - faceBox.y1) * 2 / 3 + val interX = + (max(0.0, (roiRect.left - faceLeft).toDouble()) + max( + 0.0, + (faceRight - roiRect.right).toDouble() + )).toFloat() + val interY = + (max(0.0, (roiRect.top - topY).toDouble()) + max( + 0.0, + (faceBottom - roiRect.bottom).toDouble() + )).toFloat() + if (interX / roiRect.width() > interRate || interY / roiRect.height() > interRate) { + return FACE_CAPTURE_STATE.FIT_IN_CIRCLE + } + + if (interX / roiRect.width() > interRate || interY / roiRect.height() > interRate) { + return FACE_CAPTURE_STATE.FIT_IN_CIRCLE + } + + if ((faceBox.y2 - faceBox.y1) * (faceBox.x2 - faceBox.x1) < roiRect.width() * roiRect.height() * sizeRate) { + return FACE_CAPTURE_STATE.MOVE_CLOSER + } + + if (abs(faceBox.yaw.toDouble()) > yawThreshold || abs( + faceBox.roll.toDouble() + ) > rollThreshold || abs(faceBox.pitch.toDouble()) > pitchThreshold + ) { + return FACE_CAPTURE_STATE.NO_FRONT + } + + if (faceBox.face_occlusion > 0.5) { + return FACE_CAPTURE_STATE.FACE_OCCLUDED + } + + if (faceBox.left_eye_closed > 0.5 || + faceBox.right_eye_closed > 0.5 + ) { + return FACE_CAPTURE_STATE.EYE_CLOSED + } + + if (faceBox.mouth_opened > 0.5) { + return FACE_CAPTURE_STATE.MOUTH_OPENED + } + + return FACE_CAPTURE_STATE.CAPTURE_OK + } + + inner class FaceFrameProcessor : FrameProcessor { + + override fun process(frame: Frame) { + + if(captureView.viewMode == CaptureView.VIEW_MODE.NO_FACE_PREPARE) { + return + } + + var cameraMode = 7 + val bitmap = FaceSDK.yuv2Bitmap(frame.image, frame.size.width, frame.size.height, cameraMode) + + val faceDetectionParam = FaceDetectionParam() + faceDetectionParam.check_face_occlusion = true + faceDetectionParam.check_eye_closeness = true + faceDetectionParam.check_mouth_opened = true + val faceBoxes = FaceSDK.faceDetection(bitmap, faceDetectionParam) + + val faceCaptureState = checkFace(faceBoxes, context) + + if (captureView.viewMode == CaptureView.VIEW_MODE.REPEAT_NO_FACE_PREPARE) { + if (faceCaptureState.compareTo(FACE_CAPTURE_STATE.NO_FACE) > 0) { + runOnUiThread { captureView.setViewMode(CaptureView.VIEW_MODE.TO_FACE_CIRCLE) } + } + } else if (captureView.viewMode == CaptureView.VIEW_MODE.FACE_CIRCLE) { + runOnUiThread { + captureView.setFrameSize(Size(bitmap.width, bitmap.height)) + captureView.setFaceBoxes(faceBoxes) + if (faceCaptureState == FACE_CAPTURE_STATE.NO_FACE) { + warningTxt.text = "" + + captureView.setViewMode(CaptureView.VIEW_MODE.FACE_CIRCLE_TO_NO_FACE) + } else if (faceCaptureState == FACE_CAPTURE_STATE.MULTIPLE_FACES) warningTxt.text = + "Multiple face detected!" + else if (faceCaptureState == FACE_CAPTURE_STATE.FIT_IN_CIRCLE) warningTxt.text = + "Fit in circle!" + else if (faceCaptureState == FACE_CAPTURE_STATE.MOVE_CLOSER) warningTxt.text = + "Move closer!" + else if (faceCaptureState == FACE_CAPTURE_STATE.NO_FRONT) warningTxt.text = + "Not fronted face!" + else if (faceCaptureState == FACE_CAPTURE_STATE.FACE_OCCLUDED) warningTxt.text = + "Face occluded!" + else if (faceCaptureState == FACE_CAPTURE_STATE.EYE_CLOSED) warningTxt.text = + "Eye closed!" + else if (faceCaptureState == FACE_CAPTURE_STATE.MOUTH_OPENED) warningTxt.text = + "Mouth opened!" + else if (faceCaptureState == FACE_CAPTURE_STATE.SPOOFED_FACE) warningTxt.text = + "Spoof face" + else { + warningTxt.text = "" + captureView.setViewMode(CaptureView.VIEW_MODE.FACE_CAPTURE_PREPARE) + + capturedBitmap = bitmap + capturedFace = faceBoxes[0] + captureView.setCapturedBitmap(capturedBitmap) + } + } + } else if (captureView.viewMode == CaptureView.VIEW_MODE.FACE_CAPTURE_PREPARE) { + if (faceCaptureState == FACE_CAPTURE_STATE.CAPTURE_OK) { + if (faceBoxes[0].face_quality > capturedFace!!.face_quality) { + capturedBitmap = bitmap + capturedFace = faceBoxes[0] + captureView.setCapturedBitmap(capturedBitmap) + } + } + } else if (captureView.viewMode == CaptureView.VIEW_MODE.FACE_CAPTURE_DONE) { + runOnUiThread { fotoapparat.stop() } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbyai/faceattribute/CaptureView.java b/app/src/main/java/com/kbyai/faceattribute/CaptureView.java new file mode 100644 index 0000000..db140e8 --- /dev/null +++ b/app/src/main/java/com/kbyai/faceattribute/CaptureView.java @@ -0,0 +1,571 @@ +package com.kbyai.faceattribute; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.SweepGradient; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Size; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; + +import com.kbyai.facesdk.FaceBox; + +import java.util.List; + +public class CaptureView extends View implements Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener { + + enum VIEW_MODE { + MODE_NONE, + NO_FACE_PREPARE, + REPEAT_NO_FACE_PREPARE, + TO_FACE_CIRCLE, + FACE_CIRCLE_TO_NO_FACE, + FACE_CIRCLE, + FACE_CAPTURE_PREPARE, + FACE_CAPTURE_DONE, + } + + private Context context; + + private Paint scrimPaint; + + private Paint eraserPaint; + + private Paint outSideRoundPaint; + + private Paint outSideRoundNonPaint; + + private Paint outSideActiveRoundPaint; + + private Paint outSideRoundNonFacePaint; + + private Paint outSideRoundFacePaint; + + private Paint outSideRoundActiveFacePaint; + private boolean scrimInited; + private Size frameSize = new Size(720, 1280); + + private List faceBoxes; + + private float animateValue; + private ValueAnimator valueAnimator; + + public VIEW_MODE viewMode = VIEW_MODE.MODE_NONE; + + private int repeatCount = 0; + + private ViewModeChanged viewModeInterface; + + private Bitmap capturedBitmap; + + private Bitmap roiBitmap; + + interface ViewModeChanged + { + public void view5_finished(); + } + + public void setViewModeInterface(ViewModeChanged viewMode) { + viewModeInterface = viewMode; + } + + public CaptureView(Context context) { + this(context, null); + + this.context = context; + init(); + } + + public CaptureView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + this.context = context; + + init(); + } + + public void init() { + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + + scrimPaint = new Paint(); + + eraserPaint = new Paint(); + eraserPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + + outSideRoundPaint = new Paint(); + outSideRoundPaint.setStyle(Paint.Style.STROKE); + outSideRoundPaint.setStrokeWidth(7); + outSideRoundPaint.setColor(ContextCompat.getColor(context, R.color.md_theme_dark_errorContainer)); + outSideRoundPaint.setAntiAlias(true); + + outSideRoundNonPaint = new Paint(); + outSideRoundNonPaint.setStyle(Paint.Style.STROKE); + outSideRoundNonPaint.setStrokeWidth(2); + outSideRoundNonPaint.setColor(ContextCompat.getColor(context, R.color.md_theme_dark_inverseOnSurface)); + outSideRoundNonPaint.setAntiAlias(true); + + outSideActiveRoundPaint = new Paint(); + outSideActiveRoundPaint.setStyle(Paint.Style.STROKE); + outSideActiveRoundPaint.setStrokeWidth(8); + outSideActiveRoundPaint.setColor(ContextCompat.getColor(context, R.color.md_theme_dark_onSurface)); + outSideActiveRoundPaint.setAntiAlias(true); + + outSideRoundNonFacePaint = new Paint(); + outSideRoundNonFacePaint.setStyle(Paint.Style.STROKE); + outSideRoundNonFacePaint.setStrokeWidth(10); + outSideRoundNonFacePaint.setColor(ContextCompat.getColor(context, R.color.md_theme_dark_inverseOnSurface)); + outSideRoundNonFacePaint.setAntiAlias(true); + + outSideRoundFacePaint = new Paint(); + outSideRoundFacePaint.setStyle(Paint.Style.STROKE); + outSideRoundFacePaint.setStrokeWidth(10); + outSideRoundFacePaint.setColor(ContextCompat.getColor(context, R.color.md_theme_dark_errorContainer)); + outSideRoundFacePaint.setAntiAlias(true); + + outSideRoundActiveFacePaint = new Paint(); + outSideRoundActiveFacePaint.setStyle(Paint.Style.STROKE); + outSideRoundActiveFacePaint.setStrokeWidth(10); + outSideRoundActiveFacePaint.setColor(ContextCompat.getColor(context, R.color.md_theme_dark_onPrimaryContainer)); + outSideRoundActiveFacePaint.setAntiAlias(true); + } + + public void setFrameSize(Size frameSize) + { + this.frameSize = frameSize; + } + + public void setFaceBoxes(List faceBoxes) + { + this.faceBoxes = faceBoxes; + invalidate(); + } + + public void setCapturedBitmap(Bitmap bitmap) { + capturedBitmap = bitmap; + + RectF roiRect = CaptureView.getROIRect1(frameSize); + + float ratioView = getWidth() / (float)getHeight(); + float ratioFrame = frameSize.getWidth() / (float)frameSize.getHeight(); + RectF roiViewRect = new RectF(); + + if(ratioView < ratioFrame) { + float dx = ((getHeight() * ratioFrame) - getWidth()) / 2; + float dy = 0f; + float ratio = getHeight() / (float)frameSize.getHeight(); + + float x1 = roiRect.left * ratio - dx; + float y1 = roiRect.top * ratio - dy; + float x2 = roiRect.right * ratio - dx; + float y2 = roiRect.bottom * ratio - dy; + + roiViewRect = new RectF(x1, y1, x2, y2); + } else { + float dx = 0; + float dy = ((getWidth() / ratioFrame) - getHeight()) / 2; + float ratio = getHeight() / (float)frameSize.getHeight(); + + float x1 = roiRect.left * ratio - dx; + float y1 = roiRect.top * ratio - dy; + float x2 = roiRect.right * ratio - dx; + float y2 = roiRect.bottom * ratio - dy; + + roiViewRect = new RectF(x1, y1, x2, y2); + } + + Rect roiRectSrc = new Rect(); + Rect roiViewRectSrc = new Rect(); + roiRect.round(roiRectSrc); + roiViewRect.round(roiViewRectSrc); + roiBitmap = Bitmap.createBitmap(roiRectSrc.width(), roiRectSrc.height(), Bitmap.Config.ARGB_8888); + + final Path path = new Path(); + path.addCircle( + (float) (roiRectSrc.width() / 2) + , (float) (roiRectSrc.height() / 2) + , (float) Math.min(roiRectSrc.width(), (roiRectSrc.height() / 2)) + , Path.Direction.CCW + ); + + final Canvas canvas1 = new Canvas(roiBitmap); + canvas1.clipPath(path); + canvas1.drawBitmap(capturedBitmap, roiRectSrc, new Rect(0, 0, roiRectSrc.width(), roiRectSrc.height()), null); + } + + public void setViewMode(VIEW_MODE mode) { + this.viewMode = mode; + + if(valueAnimator != null) { + valueAnimator.pause(); + } + + if(this.viewMode == VIEW_MODE.NO_FACE_PREPARE) { + ValueAnimator animator = ValueAnimator.ofFloat(1.4f, 0.88f); + animator.addUpdateListener(this); + animator.addListener(this); + animator.setDuration(800); + + valueAnimator = animator; + } else if(viewMode == VIEW_MODE.REPEAT_NO_FACE_PREPARE) { + ValueAnimator animator = ValueAnimator.ofFloat(0.88f, 0.92f); + animator.addUpdateListener(this); + animator.addListener(this); + animator.setRepeatMode(ValueAnimator.REVERSE); + animator.setRepeatCount(-1); + animator.setDuration(1300); + + valueAnimator = animator; + } else if(viewMode == VIEW_MODE.TO_FACE_CIRCLE) { + ValueAnimator animator = ValueAnimator.ofFloat(1.4f, 0.0f); + animator.addUpdateListener(this); + animator.addListener(this); + animator.setDuration(800); + + valueAnimator = animator; + } else if(viewMode == VIEW_MODE.FACE_CIRCLE_TO_NO_FACE) { + ValueAnimator animator = ValueAnimator.ofFloat(0f, 1.0f); + animator.addUpdateListener(this); + animator.addListener(this); + animator.setDuration(600); + + valueAnimator = animator; + } else if(viewMode == VIEW_MODE.FACE_CIRCLE) { + invalidate(); + return; + } else if(viewMode == VIEW_MODE.FACE_CAPTURE_PREPARE) { + ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 1.0f); + animator.addUpdateListener(this); + animator.addListener(this); + animator.setDuration(500); + + valueAnimator = animator; + } else if(viewMode == VIEW_MODE.FACE_CAPTURE_DONE) { + ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 1.0f); + animator.addUpdateListener(this); + animator.addListener(this); + animator.setDuration(500); + } + + valueAnimator.start(); + } + + @Override + public void onAnimationUpdate(@NonNull ValueAnimator valueAnimator) { + float value = (float)valueAnimator.getAnimatedValue(); + animateValue = value; + invalidate(); + } + + @Override + public void onAnimationStart(@NonNull Animator animator) { + repeatCount = 0; + } + + @Override + public void onAnimationEnd(@NonNull Animator animator) { + if(viewMode == VIEW_MODE.NO_FACE_PREPARE) { + setViewMode(VIEW_MODE.REPEAT_NO_FACE_PREPARE); + } else if(viewMode == VIEW_MODE.TO_FACE_CIRCLE) { + setViewMode(VIEW_MODE.FACE_CIRCLE); + } else if(viewMode == VIEW_MODE.FACE_CIRCLE_TO_NO_FACE) { + setViewMode(VIEW_MODE.NO_FACE_PREPARE); + } else if(viewMode == VIEW_MODE.FACE_CAPTURE_PREPARE) { + setViewMode(VIEW_MODE.FACE_CAPTURE_DONE); + } else if(viewMode == VIEW_MODE.FACE_CAPTURE_DONE) { + if(viewModeInterface != null) { + viewModeInterface.view5_finished(); + } + } + } + + @Override + public void onAnimationCancel(@NonNull Animator animator) { + } + + @Override + public void onAnimationRepeat(@NonNull Animator animator) { + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if(scrimInited == false) { + scrimInited = true; + scrimPaint.setShader( + new LinearGradient( + 0, + 0, + canvas.getWidth(), + canvas.getHeight(), + context.getColor(R.color.md_theme_dark_surface), + context.getColor(R.color.md_theme_dark_scrim), + Shader.TileMode.CLAMP)); + } + + if(viewMode == VIEW_MODE.FACE_CIRCLE || + viewMode == VIEW_MODE.FACE_CAPTURE_PREPARE || + viewMode == VIEW_MODE.FACE_CAPTURE_DONE || + (viewMode == VIEW_MODE.TO_FACE_CIRCLE && animateValue < 1.0f) || + viewMode == VIEW_MODE.FACE_CIRCLE_TO_NO_FACE) { + + if(viewMode == VIEW_MODE.FACE_CIRCLE_TO_NO_FACE) { + scrimPaint.setAlpha((int)((1 - animateValue) * 255)); + } else { + scrimPaint.setAlpha(255); + } + canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), scrimPaint); + } + + RectF roiRect = CaptureView.getROIRect1(frameSize); + + float ratioView = canvas.getWidth() / (float)canvas.getHeight(); + float ratioFrame = frameSize.getWidth() / (float)frameSize.getHeight(); + RectF roiViewRect = new RectF(); + + if(ratioView < ratioFrame) { + float dx = ((canvas.getHeight() * ratioFrame) - canvas.getWidth()) / 2; + float dy = 0f; + float ratio = canvas.getHeight() / (float)frameSize.getHeight(); + + float x1 = roiRect.left * ratio - dx; + float y1 = roiRect.top * ratio - dy; + float x2 = roiRect.right * ratio - dx; + float y2 = roiRect.bottom * ratio - dy; + + roiViewRect = new RectF(x1, y1, x2, y2); + } else { + float dx = 0; + float dy = ((canvas.getWidth() / ratioFrame) - canvas.getHeight()) / 2; + float ratio = canvas.getHeight() / (float)frameSize.getHeight(); + + float x1 = roiRect.left * ratio - dx; + float y1 = roiRect.top * ratio - dy; + float x2 = roiRect.right * ratio - dx; + float y2 = roiRect.bottom * ratio - dy; + + roiViewRect = new RectF(x1, y1, x2, y2); + } + + if(viewMode == VIEW_MODE.NO_FACE_PREPARE || + viewMode == VIEW_MODE.REPEAT_NO_FACE_PREPARE || + viewMode == VIEW_MODE.TO_FACE_CIRCLE || + viewMode == VIEW_MODE.FACE_CIRCLE_TO_NO_FACE) { + + RectF scaleRoiRect = roiViewRect; + if(viewMode == VIEW_MODE.NO_FACE_PREPARE || + viewMode == VIEW_MODE.REPEAT_NO_FACE_PREPARE || + (viewMode == VIEW_MODE.TO_FACE_CIRCLE && animateValue > 1.0f)) { + CaptureView.scale(scaleRoiRect, animateValue); + } + + float lineWidth1 = scaleRoiRect.width() / 5; + float lineWidthOffset1 = 0; + if(viewMode == VIEW_MODE.FACE_CIRCLE || + (viewMode == VIEW_MODE.TO_FACE_CIRCLE && animateValue < 1.0f) || + viewMode == VIEW_MODE.FACE_CIRCLE_TO_NO_FACE) { + lineWidth1 = lineWidth1 * animateValue; + lineWidthOffset1 = scaleRoiRect.width() / 2 * (1 - animateValue); + } + float lineHeight1 = scaleRoiRect.height() / 5; + float lineHeightOffset1 = 0; + if(viewMode == VIEW_MODE.FACE_CIRCLE || + (viewMode == VIEW_MODE.TO_FACE_CIRCLE && animateValue < 1.0f) || + viewMode == VIEW_MODE.FACE_CIRCLE_TO_NO_FACE) { + lineHeight1 = lineHeight1 * animateValue; + lineHeightOffset1 = scaleRoiRect.height() / 2 * (1 - animateValue); + } + float quad_r1 = scaleRoiRect.width() / 12; + if(viewMode == VIEW_MODE.FACE_CIRCLE || + (viewMode == VIEW_MODE.TO_FACE_CIRCLE && animateValue < 1.0f) || + viewMode == VIEW_MODE.FACE_CIRCLE_TO_NO_FACE) { + quad_r1 = scaleRoiRect.width() / 12 + (scaleRoiRect.width() / 2 - scaleRoiRect.width() / 12) * (1 - animateValue) - 20; + } + + Paint paint1 = new Paint(); + paint1.setStyle(Paint.Style.STROKE); + paint1.setStrokeWidth(10); + paint1.setColor(ContextCompat.getColor(context, R.color.md_theme_dark_onPrimaryContainer)); + if(viewMode == VIEW_MODE.NO_FACE_PREPARE || + (viewMode == VIEW_MODE.TO_FACE_CIRCLE && animateValue > 1.0f)) { + int alpha = Math.min(255, (int)((1.4 - animateValue) / 0.4 * 255)); + paint1.setAlpha(alpha); + } else { + paint1.setAlpha(255); + } + paint1.setAntiAlias(true); + + Path path1 = new Path(); + path1.moveTo(scaleRoiRect.left, scaleRoiRect.top + lineHeight1 + lineHeightOffset1); + path1.lineTo(scaleRoiRect.left, scaleRoiRect.top + quad_r1); + path1.arcTo(scaleRoiRect.left, scaleRoiRect.top, scaleRoiRect.left + quad_r1 * 2, scaleRoiRect.top + quad_r1 * 2, 180, 90, false); + path1.lineTo(scaleRoiRect.left + lineWidth1 + lineWidthOffset1, scaleRoiRect.top); + canvas.drawPath(path1, paint1); + + Path path2 = new Path(); + path2.moveTo(scaleRoiRect.right, scaleRoiRect.top + lineHeight1 + lineHeightOffset1); + path2.lineTo(scaleRoiRect.right, scaleRoiRect.top + quad_r1); + path2.arcTo(scaleRoiRect.right - quad_r1 * 2, scaleRoiRect.top, scaleRoiRect.right, scaleRoiRect.top + quad_r1 * 2, 0, -90, false); + path2.lineTo(scaleRoiRect.right - lineWidth1 - lineWidthOffset1, scaleRoiRect.top); + canvas.drawPath(path2, paint1); + + Path path3 = new Path(); + path3.moveTo(scaleRoiRect.right, scaleRoiRect.bottom - lineHeight1 - lineHeightOffset1); + path3.lineTo(scaleRoiRect.right, scaleRoiRect.bottom - quad_r1); + path3.arcTo(scaleRoiRect.right - quad_r1 * 2, scaleRoiRect.bottom - quad_r1 * 2, scaleRoiRect.right, scaleRoiRect.bottom, 0, 90, false); + path3.lineTo(scaleRoiRect.right - lineWidth1 - lineWidthOffset1, scaleRoiRect.bottom); + canvas.drawPath(path3, paint1); + + Path path4 = new Path(); + path4.moveTo(scaleRoiRect.left, scaleRoiRect.bottom - lineHeight1 - lineHeightOffset1); + path4.lineTo(scaleRoiRect.left, scaleRoiRect.bottom - quad_r1); + path4.arcTo(scaleRoiRect.left, scaleRoiRect.bottom - quad_r1 * 2, scaleRoiRect.left + quad_r1 * 2, scaleRoiRect.bottom, 180, -90, false); + path4.lineTo(scaleRoiRect.left + lineWidth1 + lineWidthOffset1, roiViewRect.bottom); + canvas.drawPath(path4, paint1); + } + + if((viewMode == VIEW_MODE.TO_FACE_CIRCLE && animateValue < 1.0f) || viewMode == VIEW_MODE.FACE_CIRCLE_TO_NO_FACE) { + + float start_width = 0.8f * roiViewRect.width() * 0.5f / (float)Math.cos(45 * Math.PI / 180); + + float center_x = roiViewRect.centerX(); + float center_y = roiViewRect.centerY(); + float left = center_x - (roiViewRect.width() / 2 * (1 - animateValue) + start_width * animateValue); + float top = center_y - (roiViewRect.width() / 2 * (1 - animateValue) + start_width * animateValue); + float right = center_x + (roiViewRect.width() / 2 * (1 - animateValue) + start_width * animateValue); + float bottom = center_y + (roiViewRect.width() / 2 * (1 - animateValue) + start_width * animateValue); + RectF eraseRect = new RectF(left, top, right, bottom); + canvas.drawRoundRect(eraseRect, eraseRect.width() / 2, eraseRect.height() / 2, eraserPaint); + } else if(viewMode == VIEW_MODE.FACE_CIRCLE) { + canvas.drawRoundRect(roiViewRect, roiViewRect.width() / 2, roiViewRect.height() / 2, eraserPaint); + + double centerX = roiViewRect.centerX(); + double centerY = roiViewRect.centerY(); + + for(int i = 0; i < 360; i += 5) { + + double a1 = roiViewRect.width() / 2 + 10; + double b1 = roiViewRect.height() / 2 + 10; + double a2 = roiViewRect.width() / 2 + 40; + double b2 = roiViewRect.height() / 2 + 40; + + double th = i * Math.PI / 180; + double x1 = a1 * b1 / Math.sqrt(Math.pow(b1, 2) + Math.pow(a1, 2) * Math.tan(th) * Math.tan(th)); + double x2 = a2 * b2 / Math.sqrt(Math.pow(b2, 2) + Math.pow(a2, 2) * Math.tan(th) * Math.tan(th)); + double y1 = Math.sqrt(1 - (x1 / a1) * (x1 / a1)) * b1; + double y2 = Math.sqrt(1 - (x1 / a1) * (x1 / a1)) * b2; + + if((i % 360) > 90 && (i % 360) < 270) { + x1 = -x1; + x2 = -x2; + } + + if((i % 360) > 180 && (i % 360) < 360) { + y1 = -y1; + y2 = -y2; + } + + canvas.drawLine((float)(centerX + x1), (float)(centerY - y1), (float)(centerX + x2), (float)(centerY - y2), outSideActiveRoundPaint); + } + + if(faceBoxes != null && faceBoxes.size() > 0) { + Paint paint1 = new Paint(); + paint1.setStyle(Paint.Style.FILL_AND_STROKE); + paint1.setStrokeWidth(6); + paint1.setColor(ContextCompat.getColor(context, R.color.md_theme_dark_onPrimaryContainer)); + paint1.setAlpha(128); + paint1.setAntiAlias(true); + + FaceBox faceBox = faceBoxes.get(0); + double yaw = faceBox.yaw; + double pitch = faceBox.pitch; + + Path path1 = new Path(); + path1.moveTo(roiViewRect.centerX(), roiViewRect.top); + path1.quadTo(roiViewRect.centerX() - roiViewRect.width() * (float) Math.sin(yaw * Math.PI / 180), roiViewRect.centerY(), roiViewRect.centerX(), roiViewRect.bottom); + path1.quadTo(roiViewRect.centerX() - roiViewRect.width() * (float) Math.sin(yaw * Math.PI / 180) / 3, roiViewRect.centerY(), roiViewRect.centerX(), roiViewRect.top); + canvas.drawPath(path1, paint1); + + Path path2 = new Path(); + path2.moveTo(roiViewRect.left, roiViewRect.centerY()); + path2.quadTo(roiViewRect.centerX(), roiViewRect.centerY() + roiViewRect.width() * (float) Math.sin(pitch * Math.PI / 180), roiViewRect.right, roiViewRect.centerY()); + path2.quadTo(roiViewRect.centerX(), roiViewRect.centerY() + roiViewRect.width() * (float) Math.sin(pitch * Math.PI / 180) / 3, roiViewRect.left, roiViewRect.centerY()); + canvas.drawPath(path2, paint1); + } + } else if(viewMode == VIEW_MODE.FACE_CAPTURE_PREPARE) { + + RectF borderRect = new RectF(roiViewRect); + CaptureView.scale(borderRect, 1.04f); + Paint paint1 = new Paint(); + paint1.setStyle(Paint.Style.FILL); + paint1.setColor(ContextCompat.getColor(context, R.color.md_theme_dark_onTertiary)); + paint1.setAntiAlias(true); + canvas.drawCircle(borderRect.centerX(), borderRect.centerY(), borderRect.width() / 2, paint1); + + RectF innerRect = new RectF(roiViewRect); + CaptureView.scale(innerRect, 1.0f - animateValue); + canvas.drawRoundRect(innerRect, innerRect.width() / 2, innerRect.height() / 2, eraserPaint); + } else if(viewMode == VIEW_MODE.FACE_CAPTURE_DONE) { + RectF borderRect = new RectF(roiViewRect); + CaptureView.scale(borderRect, 0.8f); + + Rect roiViewRectSrc = new Rect(); + borderRect.round(roiViewRectSrc); + + Paint paint1 = new Paint(); + paint1.setStyle(Paint.Style.STROKE); + paint1.setColor(ContextCompat.getColor(context, R.color.md_theme_dark_onTertiary)); + paint1.setStrokeWidth(15); + paint1.setAntiAlias(true); + + canvas.translate(0, (getWidth() / 5 - roiViewRect.top) * animateValue); + canvas.drawBitmap(roiBitmap, new Rect(0, 0, roiBitmap.getWidth(), roiBitmap.getHeight()), borderRect, null); + canvas.drawCircle(borderRect.centerX(), borderRect.centerY(), borderRect.width() / 2, paint1); + } + } + + private static void scale(RectF rect, float factor){ + float diffHorizontal = (rect.right-rect.left) * (factor-1f); + float diffVertical = (rect.bottom-rect.top) * (factor-1f); + + rect.top -= diffVertical/2f; + rect.bottom += diffVertical/2f; + + rect.left -= diffHorizontal/2f; + rect.right += diffHorizontal/2f; + } + + public static RectF getROIRect(Size frameSize) { + int margin = frameSize.getWidth() / 6; + int rectHeight = (frameSize.getWidth() - 2 * margin) * 6 / 5; + + RectF roiRect = new RectF(margin, (frameSize.getHeight() - rectHeight) / 2, + frameSize.getWidth() - margin, (frameSize.getHeight() - rectHeight) / 2 + rectHeight); + return roiRect; + } + + public static RectF getROIRect1(Size frameSize) { + int margin = frameSize.getWidth() / 6; + int rectHeight = (frameSize.getWidth() - 2 * margin); + + RectF roiRect = new RectF(margin, (frameSize.getHeight() - rectHeight) / 2, + frameSize.getWidth() - margin, (frameSize.getHeight() - rectHeight) / 2 + rectHeight); + return roiRect; + } +} diff --git a/app/src/main/java/com/kbyai/faceattribute/FACE_CAPTURE_STATE.java b/app/src/main/java/com/kbyai/faceattribute/FACE_CAPTURE_STATE.java new file mode 100644 index 0000000..16db429 --- /dev/null +++ b/app/src/main/java/com/kbyai/faceattribute/FACE_CAPTURE_STATE.java @@ -0,0 +1,5 @@ +package com.kbyai.faceattribute; + +public enum FACE_CAPTURE_STATE { + NO_FACE, MULTIPLE_FACES, FIT_IN_CIRCLE, MOVE_CLOSER, NO_FRONT, FACE_OCCLUDED, EYE_CLOSED, MOUTH_OPENED, SPOOFED_FACE, CAPTURE_OK +} diff --git a/app/src/main/java/com/kbyai/faceattribute/FaceRectTransformer.java b/app/src/main/java/com/kbyai/faceattribute/FaceRectTransformer.java new file mode 100644 index 0000000..2190625 --- /dev/null +++ b/app/src/main/java/com/kbyai/faceattribute/FaceRectTransformer.java @@ -0,0 +1,205 @@ +package com.kbyai.faceattribute; + +import android.graphics.Rect; +import android.util.Log; + + +public class FaceRectTransformer { + private int previewWidth, previewHeight, canvasWidth, canvasHeight, cameraDisplayOrientation; + private boolean isMirror; + private boolean mirrorHorizontal = false, mirrorVertical = false; + private int cameraId; + + 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; + } + + 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(); + switch (cameraDisplayOrientation) { + case 0: + if (cameraId == 0) { + newRect.left = canvasWidth - rect.right; + newRect.right = canvasWidth - rect.left; + } else { + newRect.left = rect.left; + newRect.right = rect.right; + } + newRect.top = rect.top; + newRect.bottom = rect.bottom; + break; + case 90: + newRect.right = canvasWidth - rect.top; + newRect.left = canvasWidth - rect.bottom; + if (cameraId == 0) { + 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 == 0) { + newRect.left = rect.left; + newRect.right = rect.right; + } else { + newRect.left = canvasWidth - rect.right; + newRect.right = canvasWidth - rect.left; + } + break; + case 270: + newRect.left = rect.top; + newRect.right = rect.bottom; + if (cameraId == 0) { + newRect.top = rect.left; + newRect.bottom = rect.right; + } else { + newRect.top = canvasHeight - rect.right; + newRect.bottom = canvasHeight - rect.left; + } + break; + default: + break; + } + + /** + * isMirror mirrorHorizontal finalIsMirrorHorizontal + * true true false + * false false false + * true false true + * false true true + * + * XOR + */ + if (isMirror ^ mirrorHorizontal) { + int left = newRect.left; + int right = newRect.right; + newRect.left = canvasWidth - right; + newRect.right = canvasWidth - left; + } + if (mirrorVertical) { + int top = newRect.top; + int bottom = newRect.bottom; + newRect.top = canvasHeight - bottom; + newRect.bottom = canvasHeight - top; + } + + return newRect; + } + + public void setPreviewWidth(int previewWidth) { + this.previewWidth = previewWidth; + } + + public void setPreviewHeight(int previewHeight) { + this.previewHeight = previewHeight; + } + + public void setCanvasWidth(int canvasWidth) { + this.canvasWidth = canvasWidth; + } + + public void setCanvasHeight(int canvasHeight) { + this.canvasHeight = canvasHeight; + } + + public void setCameraDisplayOrientation(int cameraDisplayOrientation) { + this.cameraDisplayOrientation = cameraDisplayOrientation; + } + + public void setCameraId(int cameraId) { + this.cameraId = cameraId; + } + + public void setMirror(boolean mirror) { + isMirror = mirror; + } + + public int getPreviewWidth() { + return previewWidth; + } + + public int getPreviewHeight() { + return previewHeight; + } + + public int getCanvasWidth() { + return canvasWidth; + } + + public int getCanvasHeight() { + return canvasHeight; + } + + public int getCameraDisplayOrientation() { + return cameraDisplayOrientation; + } + + public int getCameraId() { + return cameraId; + } + + public boolean isMirror() { + return isMirror; + } + + public boolean isMirrorHorizontal() { + return mirrorHorizontal; + } + + public void setMirrorHorizontal(boolean mirrorHorizontal) { + this.mirrorHorizontal = mirrorHorizontal; + } + + public boolean isMirrorVertical() { + return mirrorVertical; + } + + public void setMirrorVertical(boolean mirrorVertical) { + this.mirrorVertical = mirrorVertical; + } +} diff --git a/app/src/main/java/com/kbyai/faceattribute/FaceRectView.java b/app/src/main/java/com/kbyai/faceattribute/FaceRectView.java new file mode 100644 index 0000000..7058b31 --- /dev/null +++ b/app/src/main/java/com/kbyai/faceattribute/FaceRectView.java @@ -0,0 +1,181 @@ +package com.kbyai.faceattribute; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + + +public class FaceRectView extends View { + + public enum DispState { + NO_FACE, + FACE_DETECTED, + ROUND_NORMAL, + ROUND_ZOOM_IN, + ROUND_ZOOM_OUT + }; + + private Paint paint; + private static final int DEFAULT_FACE_RECT_THICKNESS = 6; + + private Paint scrimPaint; + private Paint noFaceScrimPaint; + private Paint outSideScrimPaint; + private Paint eraserPaint; + private Paint boxPaint; + private int mShader = 0; + private DispState mMode = DispState.NO_FACE; + + private boolean hasFace = false; + + @ColorInt + private int boxGradientStartColor; + @ColorInt + private int boxGradientEndColor; + + Context mContext; + + public FaceRectView(Context context) { + this(context, null); + + mContext = context; + init(); + } + + public FaceRectView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + mContext = context; + paint = new Paint(); + + init(); + } + + public void setHasFace(boolean hasFace) { + this.hasFace = hasFace; + } + + public void setMode(DispState mode) { + mMode = mode; + postInvalidate(); + } + + public void init() { + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + + scrimPaint = new Paint(); + noFaceScrimPaint = new Paint(); + outSideScrimPaint = new Paint(); + // Sets up a gradient background color at vertical. + + eraserPaint = new Paint(); + eraserPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + + boxPaint = new Paint(); + boxPaint.setStyle(Paint.Style.STROKE); + boxPaint.setStrokeWidth(1); + boxPaint.setColor(Color.WHITE); + + boxGradientStartColor = mContext.getColor(R.color.bounding_box_gradient_start); + boxGradientEndColor = mContext.getColor(R.color.md_theme_light_onPrimary); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if(getWidth() > 0 && mShader == 0) { + mShader = 1; + scrimPaint.setShader( + new LinearGradient( + 0, + 0, + getWidth(), + getHeight(), + mContext.getColor(R.color.object_confirmed_bg_gradient_start), + mContext.getColor(R.color.object_confirmed_bg_gradient_end), + Shader.TileMode.CLAMP)); + + noFaceScrimPaint.setShader( + new LinearGradient( + 0, + 0, + getWidth(), + getHeight(), + mContext.getColor(R.color.bg_gradient_noface_start), + mContext.getColor(R.color.bg_gradient_noface_end), + Shader.TileMode.CLAMP)); + } + + RectF rect = new RectF(); + if(mMode == DispState.ROUND_NORMAL) { + int margin = getWidth() / 6; + int rectHeight = (getWidth() - 2 * margin) * 4 / 3; + canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), scrimPaint); + + rect = new RectF(margin, (getHeight() - rectHeight) / 2, getWidth() - margin, (getHeight() - rectHeight) / 2 + rectHeight); + canvas.drawRoundRect(rect, rect.width() / 2, rect.height() / 2, eraserPaint); + } else if(mMode == DispState.ROUND_ZOOM_OUT) { //zoom in + int margin = getWidth() / 4; + int rectHeight = (getWidth() - 2 * margin) * 4 / 3; + canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), scrimPaint); + + rect = new RectF(margin, (getHeight() - rectHeight) / 2, getWidth() - margin, (getHeight() - rectHeight) / 2 + rectHeight); + canvas.drawRoundRect(rect, rect.width() / 2, rect.height() / 2, eraserPaint); + } else if(mMode == DispState.ROUND_ZOOM_IN) { //zoom out + int margin = getWidth() / 15; + int rectHeight = getWidth() * 7 / 5; + canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), scrimPaint); + + rect = new RectF(margin, (getHeight() - rectHeight) / 2, getWidth() - margin, (getHeight() - rectHeight) / 2 + rectHeight); + canvas.drawRoundRect(rect, rect.width() / 2, rect.height() / 2, eraserPaint); + } else if(mMode == DispState.FACE_DETECTED || mMode == DispState.NO_FACE) { + int margin = getWidth() / 6; + int rectHeight = (getWidth() - 2 * margin) * 4 / 3; + canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), noFaceScrimPaint); + + rect = new RectF(margin, (getHeight() - rectHeight) / 2, getWidth() - margin, (getHeight() - rectHeight) / 2 + rectHeight); + + if(hasFace) { + outSideScrimPaint.setStyle(Paint.Style.STROKE); + outSideScrimPaint.setStrokeWidth(30); + outSideScrimPaint.setColor(Color.GREEN); + outSideScrimPaint.setAntiAlias(true); + + canvas.drawRoundRect(rect, 50, 50, outSideScrimPaint); + } + canvas.drawRoundRect(rect, 50, 50, eraserPaint); + } + + // Draws the bounding box with a gradient border color at vertical. + if(mMode != DispState.NO_FACE) { + boxPaint.setShader( + new LinearGradient( + rect.left, + rect.top, + rect.left, + rect.bottom, + boxGradientStartColor, + boxGradientEndColor, + Shader.TileMode.CLAMP)); + canvas.drawRoundRect(rect, rect.width() / 2, rect.height() / 2, boxPaint); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kbyai/faceattribute/FaceView.java b/app/src/main/java/com/kbyai/faceattribute/FaceView.java new file mode 100644 index 0000000..705e1c0 --- /dev/null +++ b/app/src/main/java/com/kbyai/faceattribute/FaceView.java @@ -0,0 +1,108 @@ +package com.kbyai.faceattribute; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Size; +import android.view.View; + +import androidx.annotation.Nullable; + +import com.kbyai.faceattribute.SettingsActivity; +import com.kbyai.facesdk.FaceBox; + +import java.util.List; + +public class FaceView extends View { + + private Context context; + private Paint realPaint; + private Paint spoofPaint; + + private Size frameSize; + + private List faceBoxes; + + public FaceView(Context context) { + this(context, null); + + this.context = context; + init(); + } + + public FaceView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + this.context = context; + + init(); + } + + public void init() { + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + + realPaint = new Paint(); + realPaint.setStyle(Paint.Style.STROKE); + realPaint.setStrokeWidth(3); + realPaint.setColor(Color.GREEN); + realPaint.setAntiAlias(true); + realPaint.setTextSize(50); + + spoofPaint = new Paint(); + spoofPaint.setStyle(Paint.Style.STROKE); + spoofPaint.setStrokeWidth(3); + spoofPaint.setColor(Color.RED); + spoofPaint.setAntiAlias(true); + spoofPaint.setTextSize(50); + } + + public void setFrameSize(Size frameSize) + { + this.frameSize = frameSize; + } + + public void setFaceBoxes(List faceBoxes) + { + this.faceBoxes = faceBoxes; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (frameSize != null && faceBoxes != null) { + float x_scale = this.frameSize.getWidth() / (float)canvas.getWidth(); + float y_scale = this.frameSize.getHeight() / (float)canvas.getHeight(); + + for (int i = 0; i < faceBoxes.size(); i++) { + FaceBox faceBox = faceBoxes.get(i); + + if (faceBox.liveness < 0.7) + { + spoofPaint.setStrokeWidth(3); + spoofPaint.setStyle(Paint.Style.FILL_AND_STROKE); + canvas.drawText("SPOOF " + faceBox.liveness, (faceBox.x1 / x_scale) + 10, (faceBox.y1 / y_scale) - 30, spoofPaint); + + spoofPaint.setStrokeWidth(5); + spoofPaint.setStyle(Paint.Style.STROKE); + canvas.drawRect(new Rect((int)(faceBox.x1 / x_scale), (int)(faceBox.y1 / y_scale), + (int)(faceBox.x2 / x_scale), (int)(faceBox.y2 / y_scale)), spoofPaint); + } + else + { + realPaint.setStrokeWidth(3); + realPaint.setStyle(Paint.Style.FILL_AND_STROKE); + canvas.drawText("REAL " + faceBox.liveness, (faceBox.x1 / x_scale) + 10, (faceBox.y1 / y_scale) - 30, realPaint); + + realPaint.setStyle(Paint.Style.STROKE); + realPaint.setStrokeWidth(5); + canvas.drawRect(new Rect((int)(faceBox.x1 / x_scale), (int)(faceBox.y1 / y_scale), + (int)(faceBox.x2 / x_scale), (int)(faceBox.y2 / y_scale)), realPaint); + } + } + } + } +} diff --git a/app/src/main/java/com/kbyai/faceattribute/FloatEditTextPreference.kt b/app/src/main/java/com/kbyai/faceattribute/FloatEditTextPreference.kt new file mode 100644 index 0000000..74b29a9 --- /dev/null +++ b/app/src/main/java/com/kbyai/faceattribute/FloatEditTextPreference.kt @@ -0,0 +1,18 @@ +package com.ttv.facerecog + +import android.content.Context +import android.text.InputType +import android.util.AttributeSet +import androidx.preference.EditTextPreference + +class FloatEditTextPreference(context: Context?, attrs: AttributeSet?) : + EditTextPreference(context!!, attrs) { + + override fun getPersistedString(defaultReturnValue: String?): String? { + return getPersistedFloat(0.0f).toString() + } + + override fun persistString(value: String?): Boolean { + return persistFloat(value!!.toFloat()) + } +} diff --git a/app/src/main/java/com/kbyai/faceattribute/HttpPostMultipart.java b/app/src/main/java/com/kbyai/faceattribute/HttpPostMultipart.java new file mode 100644 index 0000000..c0e4bea --- /dev/null +++ b/app/src/main/java/com/kbyai/faceattribute/HttpPostMultipart.java @@ -0,0 +1,128 @@ +package com.kbyai.faceattribute; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.Iterator; +import java.util.Map; +import java.util.UUID; + +public class HttpPostMultipart { + private final String boundary; + private static final String LINE = "\r\n"; + private HttpURLConnection httpConn; + private String charset; + private OutputStream outputStream; + private PrintWriter writer; + + /** + * This constructor initializes a new HTTP POST request with content type + * is set to multipart/form-data + * + * @param requestURL + * @param charset + * @param headers + * @throws IOException + */ + public HttpPostMultipart(String requestURL, String charset, Map headers) throws IOException { + this.charset = charset; + boundary = UUID.randomUUID().toString(); + URL url = new URL(requestURL); + httpConn = (HttpURLConnection) url.openConnection(); + httpConn.setUseCaches(false); + httpConn.setDoOutput(true); // indicates POST method + httpConn.setDoInput(true); + httpConn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); + if (headers != null && headers.size() > 0) { + Iterator it = headers.keySet().iterator(); + while (it.hasNext()) { + String key = it.next(); + String value = headers.get(key); + httpConn.setRequestProperty(key, value); + } + } + outputStream = httpConn.getOutputStream(); + writer = new PrintWriter(new OutputStreamWriter(outputStream, charset), true); + } + + /** + * Adds a form field to the request + * + * @param name field name + * @param value field value + */ + public void addFormField(String name, String value) { + writer.append("--" + boundary).append(LINE); + writer.append("Content-Disposition: form-data; name=\"" + name + "\"").append(LINE); + writer.append("Content-Type: text/plain; charset=" + charset).append(LINE); + writer.append(LINE); + writer.append(value).append(LINE); + writer.flush(); + } + + /** + * Adds a upload file section to the request + * + * @param fieldName + * @param uploadFile + * @throws IOException + */ + public void addFilePart(String fieldName, File uploadFile) + throws IOException { + String fileName = uploadFile.getName(); + writer.append("--" + boundary).append(LINE); + writer.append("Content-Disposition: form-data; name=\"" + fieldName + "\"; filename=\"" + fileName + "\"").append(LINE); + writer.append("Content-Type: " + URLConnection.guessContentTypeFromName(fileName)).append(LINE); + writer.append("Content-Transfer-Encoding: binary").append(LINE); + writer.append(LINE); + writer.flush(); + + FileInputStream inputStream = new FileInputStream(uploadFile); + byte[] buffer = new byte[4096]; + int bytesRead = -1; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + outputStream.flush(); + inputStream.close(); + writer.append(LINE); + writer.flush(); + } + + /** + * Completes the request and receives response from the server. + * + * @return String as response in case the server returned + * status OK, otherwise an exception is thrown. + * @throws IOException + */ + public String finish() throws IOException { + String response = ""; + writer.flush(); + writer.append("--" + boundary + "--").append(LINE); + writer.close(); + + // checks server's status code first + int status = httpConn.getResponseCode(); + if (status == HttpURLConnection.HTTP_OK) { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + while ((length = httpConn.getInputStream().read(buffer)) != -1) { + result.write(buffer, 0, length); + } + response = result.toString(this.charset); + httpConn.disconnect(); + } else { + throw new IOException("Server returned non-OK status: " + status); + } + return response; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbyai/faceattribute/MainActivity.kt b/app/src/main/java/com/kbyai/faceattribute/MainActivity.kt new file mode 100644 index 0000000..6b2a5cd --- /dev/null +++ b/app/src/main/java/com/kbyai/faceattribute/MainActivity.kt @@ -0,0 +1,93 @@ +package com.kbyai.faceattribute + +import android.content.Intent +import android.graphics.Bitmap +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.widget.* +import androidx.appcompat.app.AppCompatActivity +import com.kbyai.faceattribute.SettingsActivity +import com.kbyai.facesdk.FaceBox +import com.kbyai.facesdk.FaceDetectionParam +import com.kbyai.facesdk.FaceSDK +import com.kbyai.faceattribute.CameraActivity +import kotlin.random.Random + +class MainActivity : AppCompatActivity() { + + companion object { + private val SELECT_PHOTO_REQUEST_CODE = 1 + private val SELECT_ATTRIBUTE_REQUEST_CODE = 2 + } + + private lateinit var textWarning: TextView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + textWarning = findViewById(R.id.textWarning) + + + var ret = FaceSDK.setActivation( + "DYv3mu71v8b9hqraZETNpg+CdriRbg1qLLMbfNqTeGdvSdbGPGudJpiVR4Tl9TEcJrZrZG+59ay6\n" + + "BtL78C1VsDGsOzSKw9ssETgVnT9DIc/LdNrqs4/o7o3nO0ZPz3iNu/P2jKkUXLo/uzh+aaVLbi55\n" + + "X9NQYhD5EHFqL2mLtGxcfqccTHLMW0MIe0Wq65hzPIrR6oh7tvtKzX5EcOOPv8UK2a3i9+MgtG4Y\n" + + "b+CHoQ0lNJhmZkpdKmcRidibJLKgwJqDPiZfwsW6C3hcrNNo6T8T+NMZ4W7rHcQKfdSr0yXtYqCr\n" + + "kaMKrGzlk8nYubwfqZGeAKSyjuL8mWWgY57I3Q==" + ) + + if (ret == FaceSDK.SDK_SUCCESS) { + ret = FaceSDK.init(assets) + } + + if (ret != FaceSDK.SDK_SUCCESS) { + textWarning.setVisibility(View.VISIBLE) + if (ret == FaceSDK.SDK_LICENSE_KEY_ERROR) { + textWarning.setText("Invalid license!") + } else if (ret == FaceSDK.SDK_LICENSE_APPID_ERROR) { + textWarning.setText("Invalid error!") + } else if (ret == FaceSDK.SDK_LICENSE_EXPIRED) { + textWarning.setText("License expired!") + } else if (ret == FaceSDK.SDK_NO_ACTIVATED) { + textWarning.setText("No activated!") + } else if (ret == FaceSDK.SDK_INIT_ERROR) { + textWarning.setText("Init error!") + } + } + + findViewById