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..675679c --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,58 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.kbyai.faceattribute' + compileSdk 33 + + defaultConfig { + applicationId "com.kbyai.faceattribute" + minSdk 24 + targetSdk 33 + versionCode 1 + versionName "1.0" + + 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 project(path: ':libfacesdk') + + 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..b805063 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ 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..2e731eb --- /dev/null +++ b/app/src/main/java/com/kbyai/faceattribute/AboutActivity.kt @@ -0,0 +1,87 @@ +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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbyai/faceattribute/AttributeActivity.kt b/app/src/main/java/com/kbyai/faceattribute/AttributeActivity.kt new file mode 100644 index 0000000..7cfc50d --- /dev/null +++ b/app/src/main/java/com/kbyai/faceattribute/AttributeActivity.kt @@ -0,0 +1,80 @@ +package com.kbyai.faceattribute + +import android.graphics.Bitmap +import android.os.Bundle +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity + +class AttributeActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_attribute) + + val faceImage = intent.getParcelableExtra("face_image") as? Bitmap + val livenessScore = intent.getFloatExtra("liveness", 0f) + val yaw = intent.getFloatExtra("yaw", 0f) + val roll = intent.getFloatExtra("roll", 0f) + val pitch = intent.getFloatExtra("pitch", 0f) + val face_quality = intent.getFloatExtra("face_quality", 0f) + val face_luminance = intent.getFloatExtra("face_luminance", 0f) + val left_eye_closed = intent.getFloatExtra("left_eye_closed", 0f) + val right_eye_closed = intent.getFloatExtra("right_eye_closed", 0f) + val face_occlusion = intent.getFloatExtra("face_occlusion", 0f) + val mouth_opened = intent.getFloatExtra("mouth_opened", 0f) + val age = intent.getIntExtra("age", 0) + val gender = intent.getIntExtra("gender", 0) + + findViewById(R.id.imageFace).setImageBitmap(faceImage) + + if (livenessScore > SettingsActivity.getLivenessThreshold(this)) { + val msg = String.format("Liveness: Real, score = %.03f", livenessScore) + findViewById(R.id.txtLiveness).text = msg + } else { + val msg = String.format("Liveness: Spoof, score = %.03f", livenessScore) + findViewById(R.id.txtLiveness).text = msg + } + + if (face_quality < 0.5f) { + val msg = String.format("Quality: Low, score = %.03f", face_quality) + findViewById(R.id.txtQuality).text = msg + } else if(face_quality < 0.75f){ + val msg = String.format("Quality: Medium, score = %.03f", face_quality) + findViewById(R.id.txtQuality).text = msg + } else { + val msg = String.format("Quality: High, score = %.03f", face_quality) + findViewById(R.id.txtQuality).text = msg + } + + var msg = String.format("Luminance: %.03f", face_luminance) + findViewById(R.id.txtLuminance).text = msg + + msg = String.format("Angles: yaw = %.03f, roll = %.03f, pitch = %.03f", yaw, roll, pitch) + findViewById(R.id.txtAngles).text = msg + + if (face_occlusion > SettingsActivity.getOcclusionThreshold(this)) { + msg = String.format("Face occluded: score = %.03f", face_occlusion) + findViewById(R.id.txtOcclusion).text = msg + } else { + msg = String.format("Face not occluded: score = %.03f", face_occlusion) + findViewById(R.id.txtOcclusion).text = msg + } + + msg = String.format("Left eye closed: %b, %.03f, Right eye closed: %b, %.03f", left_eye_closed > SettingsActivity.getEyecloseThreshold(this), + left_eye_closed, right_eye_closed > SettingsActivity.getEyecloseThreshold(this), right_eye_closed) + findViewById(R.id.txtEyeClosed).text = msg + + msg = String.format("Mouth opened: %b, %.03f", mouth_opened > SettingsActivity.getMouthopenThreshold(this), mouth_opened) + findViewById(R.id.txtMouthOpened).text = msg + + msg = String.format("Age: %d", age) + findViewById(R.id.txtAge).text = msg + + if(gender == 0) { + msg = String.format("Gender: Male") + } else { + msg = String.format("Gender: Female") + } + findViewById(R.id.txtGender).text = msg + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbyai/faceattribute/CameraActivity.java b/app/src/main/java/com/kbyai/faceattribute/CameraActivity.java new file mode 100644 index 0000000..d6f51d4 --- /dev/null +++ b/app/src/main/java/com/kbyai/faceattribute/CameraActivity.java @@ -0,0 +1,272 @@ +package com.kbyai.faceattribute; + + +import static androidx.camera.core.ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.media.Image; +import android.os.Bundle; +import android.util.Log; +import android.util.Size; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.camera.core.Camera; +import androidx.camera.core.CameraSelector; +import androidx.camera.core.ImageAnalysis; +import androidx.camera.core.ImageProxy; +import androidx.camera.core.Preview; +import androidx.camera.lifecycle.ProcessCameraProvider; +import androidx.camera.view.PreviewView; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import com.google.common.util.concurrent.ListenableFuture; +import com.kbyai.faceattribute.ResultActivity; +import com.kbyai.faceattribute.SettingsActivity; +import com.kbyai.facesdk.FaceBox; +import com.kbyai.facesdk.FaceDetectionParam; +import com.kbyai.facesdk.FaceSDK; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class CameraActivity extends AppCompatActivity { + + static String TAG = CameraActivity.class.getSimpleName(); + static int PREVIEW_WIDTH = 720; + static int PREVIEW_HEIGHT = 1280; + + private ExecutorService cameraExecutorService; + private PreviewView viewFinder; + private Preview preview = null; + private ImageAnalysis imageAnalyzer = null; + private Camera camera = null; + private CameraSelector cameraSelector = null; + private ProcessCameraProvider cameraProvider = null; + + private FaceView faceView; + + private Context context; + + private Boolean recognized = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_camera); + + context = this; + + viewFinder = findViewById(R.id.preview); + faceView = findViewById(R.id.faceView); + cameraExecutorService = Executors.newFixedThreadPool(1); + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + == PackageManager.PERMISSION_DENIED) { + + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, 1); + } else { + viewFinder.post(() -> + { + setUpCamera(); + }); + } + } + + @Override + public void onResume() { + super.onResume(); + + recognized = false; + } + + @Override + public void onPause() { + super.onPause(); + + faceView.setFaceBoxes(null); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + if(requestCode == 1) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED) { + + viewFinder.post(() -> + { + setUpCamera(); + }); + } + } + } + + private void setUpCamera() + { + ListenableFuture cameraProviderFuture = ProcessCameraProvider.getInstance(CameraActivity.this); + cameraProviderFuture.addListener(() -> { + + // CameraProvider + try { + cameraProvider = cameraProviderFuture.get(); + } catch (ExecutionException e) { + } catch (InterruptedException e) { + } + + // Build and bind the camera use cases + bindCameraUseCases(); + + }, ContextCompat.getMainExecutor(CameraActivity.this)); + } + + @SuppressLint({"RestrictedApi", "UnsafeExperimentalUsageError"}) + private void bindCameraUseCases() + { + int rotation = viewFinder.getDisplay().getRotation(); + + cameraSelector = new CameraSelector.Builder().requireLensFacing(SettingsActivity.getCameraLens(this)).build(); + + preview = new Preview.Builder() + .setTargetResolution(new Size(PREVIEW_WIDTH, PREVIEW_HEIGHT)) + .setTargetRotation(rotation) + .build(); + + imageAnalyzer = new ImageAnalysis.Builder() + .setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST) + .setTargetResolution(new Size(PREVIEW_WIDTH, PREVIEW_HEIGHT)) + // Set initial target rotation, we will have to call this again if rotation changes + // during the lifecycle of this use case + .setTargetRotation(rotation) + .build(); + + imageAnalyzer.setAnalyzer(cameraExecutorService, new FaceAnalyzer()); + + cameraProvider.unbindAll(); + + try { + camera = cameraProvider.bindToLifecycle( + this, cameraSelector, preview, imageAnalyzer); + + preview.setSurfaceProvider(viewFinder.getSurfaceProvider()); + } catch (Exception exc) { + } + } + + class FaceAnalyzer implements ImageAnalysis.Analyzer + { + @SuppressLint("UnsafeExperimentalUsageError") + @Override + public void analyze(@NonNull ImageProxy imageProxy) + { + analyzeImage(imageProxy); + } + } + + @SuppressLint("UnsafeExperimentalUsageError") + private void analyzeImage(ImageProxy imageProxy) + { + if(recognized == true) { + imageProxy.close(); + return; + } + + try + { + Image image = imageProxy.getImage(); + + Image.Plane[] planes = image.getPlanes(); + ByteBuffer yBuffer = planes[0].getBuffer(); + ByteBuffer uBuffer = planes[1].getBuffer(); + ByteBuffer vBuffer = planes[2].getBuffer(); + + int ySize = yBuffer.remaining(); + int uSize = uBuffer.remaining(); + int vSize = vBuffer.remaining(); + + byte[] nv21 = new byte[ySize + uSize + vSize]; + yBuffer.get(nv21, 0, ySize); + vBuffer.get(nv21, ySize, vSize); + uBuffer.get(nv21, ySize + vSize, uSize); + + Bitmap bitmap = FaceSDK.yuv2Bitmap(nv21, image.getWidth(), image.getHeight(), 7); + + FaceDetectionParam param = new FaceDetectionParam(); + param.check_liveness = true; + + List faceBoxes = FaceSDK.faceDetection(bitmap, param); + + runOnUiThread(new Runnable() { + @Override + public void run() { + faceView.setFrameSize(new Size(bitmap.getWidth(), bitmap.getHeight())); + faceView.setFaceBoxes(faceBoxes); + } + }); + + if(faceBoxes.size() > 0) { + FaceBox faceBox = faceBoxes.get(0); + if(faceBox.liveness > SettingsActivity.getLivenessThreshold(context)) { + byte[] templates = FaceSDK.templateExtraction(bitmap, faceBox); + + float maxSimiarlity = 0; + Person maximiarlityPerson = null; + for(Person person : DBManager.personList) { + float similarity = FaceSDK.similarityCalculation(templates, person.templates); + if(similarity > maxSimiarlity) { + maxSimiarlity = similarity; + maximiarlityPerson = person; + } + } + + if(maxSimiarlity > SettingsActivity.getIdentifyThreshold(this)) { + recognized = true; + final Person identifiedPerson = maximiarlityPerson; + final float identifiedSimilarity = maxSimiarlity; + + runOnUiThread(new Runnable() { + @Override + public void run() { + Bitmap faceImage = Utils.cropFace(bitmap, faceBox); + + Intent intent = new Intent(context, ResultActivity.class); + intent.putExtra("identified_face", faceImage); + intent.putExtra("enrolled_face", identifiedPerson.face); + intent.putExtra("identified_name", identifiedPerson.name); + intent.putExtra("similarity", identifiedSimilarity); + intent.putExtra("liveness", faceBox.liveness); + intent.putExtra("yaw", faceBox.yaw); + intent.putExtra("roll", faceBox.roll); + intent.putExtra("pitch", faceBox.pitch); + intent.putExtra("face_quality", faceBox.face_quality); + intent.putExtra("face_luminance", faceBox.face_luminance); + + startActivity(intent); + } + }); + } + } + } + } + catch (Exception e) + { + e.printStackTrace(); + } + finally + { + imageProxy.close(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbyai/faceattribute/CaptureActivity.java b/app/src/main/java/com/kbyai/faceattribute/CaptureActivity.java new file mode 100644 index 0000000..c6e00a7 --- /dev/null +++ b/app/src/main/java/com/kbyai/faceattribute/CaptureActivity.java @@ -0,0 +1,423 @@ +package com.kbyai.faceattribute; + + +import static androidx.camera.core.ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.RectF; +import android.media.Image; +import android.os.Bundle; +import android.text.Layout; +import android.util.Log; +import android.util.Size; +import android.view.View; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.camera.core.Camera; +import androidx.camera.core.CameraSelector; +import androidx.camera.core.ImageAnalysis; +import androidx.camera.core.ImageProxy; +import androidx.camera.core.Preview; +import androidx.camera.lifecycle.ProcessCameraProvider; +import androidx.camera.view.PreviewView; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import com.google.common.util.concurrent.ListenableFuture; +import com.kbyai.facesdk.FaceBox; +import com.kbyai.facesdk.FaceDetectionParam; +import com.kbyai.facesdk.FaceSDK; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + + + +public class CaptureActivity extends AppCompatActivity implements CaptureView.ViewModeChanged{ + + static String TAG = CaptureActivity.class.getSimpleName(); + static int PREVIEW_WIDTH = 720; + static int PREVIEW_HEIGHT = 1280; + + private ExecutorService cameraExecutorService; + private PreviewView viewFinder; + private Preview preview = null; + private ImageAnalysis imageAnalyzer = null; + private Camera camera = null; + private CameraSelector cameraSelector = null; + private ProcessCameraProvider cameraProvider = null; + + private CaptureView captureView; + + private TextView warningTxt; + + private TextView livenessTxt; + + private TextView qualityTxt; + + private TextView luminaceTxt; + + private ConstraintLayout lytCaptureResult; + + private Context context; + + private Bitmap capturedBitmap = null; + + private FaceBox capturedFace = null; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_capture); + + context = this; + + viewFinder = 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); + cameraExecutorService = Executors.newFixedThreadPool(1); + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + == PackageManager.PERMISSION_DENIED) { + + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, 1); + } else { + viewFinder.post(() -> + { + setUpCamera(); + }); + } + + captureView.setViewModeInterface(this); + captureView.setViewMode(CaptureView.VIEW_MODE.NO_FACE_PREPARE); + + findViewById(R.id.buttonEnroll).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Bitmap faceImage = Utils.cropFace(capturedBitmap, capturedFace); + byte[] templates = FaceSDK.templateExtraction(capturedBitmap, capturedFace); + + DBManager dbManager = new DBManager(context); + final int min = 10000; + final int max = 20000; + final int random = new 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(); + } + }); + } + + @Override + public void onResume() { + super.onResume(); + } + + @Override + public void onPause() { + super.onPause(); + + captureView.setFaceBoxes(null); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + if(requestCode == 1) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED) { + + viewFinder.post(() -> + { + setUpCamera(); + }); + } + } + } + + private void setUpCamera() + { + ListenableFuture cameraProviderFuture = ProcessCameraProvider.getInstance(CaptureActivity.this); + cameraProviderFuture.addListener(() -> { + + // CameraProvider + try { + cameraProvider = cameraProviderFuture.get(); + } catch (ExecutionException e) { + } catch (InterruptedException e) { + } + + // Build and bind the camera use cases + bindCameraUseCases(); + + }, ContextCompat.getMainExecutor(CaptureActivity.this)); + } + + @SuppressLint({"RestrictedApi", "UnsafeExperimentalUsageError"}) + private void bindCameraUseCases() + { + int rotation = viewFinder.getDisplay().getRotation(); + + cameraSelector = new CameraSelector.Builder().requireLensFacing(SettingsActivity.getCameraLens(this)).build(); + + preview = new Preview.Builder() + .setTargetResolution(new Size(PREVIEW_WIDTH, PREVIEW_HEIGHT)) + .setTargetRotation(rotation) + .build(); + + imageAnalyzer = new ImageAnalysis.Builder() + .setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST) + .setTargetResolution(new Size(PREVIEW_WIDTH, PREVIEW_HEIGHT)) + // Set initial target rotation, we will have to call this again if rotation changes + // during the lifecycle of this use case + .setTargetRotation(rotation) + .build(); + + imageAnalyzer.setAnalyzer(cameraExecutorService, new FaceAnalyzer()); + + cameraProvider.unbindAll(); + + try { + camera = cameraProvider.bindToLifecycle( + this, cameraSelector, preview, imageAnalyzer); + + preview.setSurfaceProvider(viewFinder.getSurfaceProvider()); + } catch (Exception exc) { + } + } + + @Override + public void view5_finished() { + + FaceDetectionParam param = new FaceDetectionParam(); + param.check_liveness = true; + + List faceBoxes = FaceSDK.faceDetection(capturedBitmap, param); + if(faceBoxes != null && faceBoxes.size() > 0) { + if(faceBoxes.get(0).liveness > SettingsActivity.getLivenessThreshold(context)) { + String msg = String.format("Liveness: Real, score = %.03f", faceBoxes.get(0).liveness); + livenessTxt.setText(msg); + } + else { + String msg = String.format("Liveness: Spoof, score = %.03f", faceBoxes.get(0).liveness); + livenessTxt.setText(msg); + } + } + + if(capturedFace.face_quality < 0.5f) { + String msg = String.format("Quality: Low, score = %.03f", capturedFace.face_quality); + qualityTxt.setText(msg); + } else if(capturedFace.face_quality < 0.75f) { + String msg = String.format("Quality: Medium, score = %.03f", capturedFace.face_quality); + qualityTxt.setText(msg); + } else { + String msg = String.format("Quality: High, score = %.03f", capturedFace.face_quality); + qualityTxt.setText(msg); + } + + String msg = String.format("Luminance: %.03f", capturedFace.face_luminance); + luminaceTxt.setText(msg); + + lytCaptureResult.setVisibility(View.VISIBLE); + } + + class FaceAnalyzer implements ImageAnalysis.Analyzer + { + @SuppressLint("UnsafeExperimentalUsageError") + @Override + public void analyze(@NonNull ImageProxy imageProxy) + { + analyzeImage(imageProxy); + } + } + + @SuppressLint("UnsafeExperimentalUsageError") + private void analyzeImage(ImageProxy imageProxy) + { + if(captureView.viewMode == CaptureView.VIEW_MODE.NO_FACE_PREPARE) { + imageProxy.close(); + return; + } + + try + { + Image image = imageProxy.getImage(); + + Image.Plane[] planes = image.getPlanes(); + ByteBuffer yBuffer = planes[0].getBuffer(); + ByteBuffer uBuffer = planes[1].getBuffer(); + ByteBuffer vBuffer = planes[2].getBuffer(); + + int ySize = yBuffer.remaining(); + int uSize = uBuffer.remaining(); + int vSize = vBuffer.remaining(); + + byte[] nv21 = new byte[ySize + uSize + vSize]; + yBuffer.get(nv21, 0, ySize); + vBuffer.get(nv21, ySize, vSize); + uBuffer.get(nv21, ySize + vSize, uSize); + + Bitmap bitmap = FaceSDK.yuv2Bitmap(nv21, image.getWidth(), image.getHeight(), 7); + + FaceDetectionParam param = new FaceDetectionParam(); + param.check_face_occlusion = true; + param.check_eye_closeness = true; + param.check_mouth_opened = true; + + List faceBoxes = FaceSDK.faceDetection(bitmap, param); + FACE_CAPTURE_STATE faceCaptureState = checkFace(faceBoxes, this); + + if(captureView.viewMode == CaptureView.VIEW_MODE.REPEAT_NO_FACE_PREPARE) { + if(faceCaptureState.compareTo(FACE_CAPTURE_STATE.NO_FACE) > 0) { + runOnUiThread(new Runnable() { + @Override + public void run() { + captureView.setViewMode(CaptureView.VIEW_MODE.TO_FACE_CIRCLE); + } + }); + } + } else if(captureView.viewMode == CaptureView.VIEW_MODE.FACE_CIRCLE) { + runOnUiThread(new Runnable() { + @Override + public void run() { + captureView.setFrameSize(new Size(bitmap.getWidth(), bitmap.getHeight())); + captureView.setFaceBoxes(faceBoxes); + + if(faceCaptureState == FACE_CAPTURE_STATE.NO_FACE) { + warningTxt.setText(""); + + captureView.setViewMode(CaptureView.VIEW_MODE.FACE_CIRCLE_TO_NO_FACE); + } + else if(faceCaptureState == FACE_CAPTURE_STATE.MULTIPLE_FACES) + warningTxt.setText("Multiple face detected!"); + else if(faceCaptureState == FACE_CAPTURE_STATE.FIT_IN_CIRCLE) + warningTxt.setText("Fit in circle!"); + else if(faceCaptureState == FACE_CAPTURE_STATE.MOVE_CLOSER) + warningTxt.setText("Move closer!"); + else if(faceCaptureState == FACE_CAPTURE_STATE.NO_FRONT) + warningTxt.setText("Not fronted face!"); + else if(faceCaptureState == FACE_CAPTURE_STATE.FACE_OCCLUDED) + warningTxt.setText("Face occluded!"); + else if(faceCaptureState == FACE_CAPTURE_STATE.EYE_CLOSED) + warningTxt.setText("Eye closed!"); + else if(faceCaptureState == FACE_CAPTURE_STATE.MOUTH_OPENED) + warningTxt.setText("Mouth opened!"); + else if(faceCaptureState == FACE_CAPTURE_STATE.SPOOFED_FACE) + warningTxt.setText("Spoof face"); + else { + warningTxt.setText(""); + captureView.setViewMode(CaptureView.VIEW_MODE.FACE_CAPTURE_PREPARE); + + capturedBitmap = bitmap; + capturedFace = faceBoxes.get(0); + captureView.setCapturedBitmap(capturedBitmap); + } + } + }); + } else if(captureView.viewMode == CaptureView.VIEW_MODE.FACE_CAPTURE_PREPARE) { + if(faceCaptureState == FACE_CAPTURE_STATE.CAPTURE_OK) { + if(faceBoxes.get(0).face_quality > capturedFace.face_quality) { + capturedBitmap = bitmap; + capturedFace = faceBoxes.get(0); + captureView.setCapturedBitmap(capturedBitmap); + } + } + } else if(captureView.viewMode == CaptureView.VIEW_MODE.FACE_CAPTURE_DONE) { + runOnUiThread(new Runnable() { + @Override + public void run() { + cameraProvider.unbindAll(); + } + }); + } + } + catch (Exception e) + { + e.printStackTrace(); + } + finally + { + imageProxy.close(); + } + } + + public static FACE_CAPTURE_STATE checkFace(List faceBoxes, Context context) { + if(faceBoxes == null || faceBoxes.size() == 0) + return FACE_CAPTURE_STATE.NO_FACE; + + if(faceBoxes.size() > 1) { + return FACE_CAPTURE_STATE.MULTIPLE_FACES; + } + + FaceBox faceBox = faceBoxes.get(0); + float faceLeft = Float.MAX_VALUE; + float faceRight = 0f; + float faceBottom = 0f; + for(int i = 0; i < 68; i ++) { + faceLeft = Math.min(faceLeft, faceBox.landmarks_68[i * 2]); + faceRight = Math.max(faceRight, faceBox.landmarks_68[i * 2]); + faceBottom = Math.max(faceBottom, faceBox.landmarks_68[i * 2 + 1]); + } + + float sizeRate = 0.30f; + float interRate = 0.03f; + Size frameSize = new Size(PREVIEW_WIDTH, PREVIEW_HEIGHT); + RectF roiRect = CaptureView.getROIRect(frameSize); + float centerY = (faceBox.y2 + faceBox.y1) / 2; + float topY = centerY - (faceBox.y2 - faceBox.y1) * 2 / 3; + float interX = Math.max(0f, roiRect.left - faceLeft) + Math.max(0f, faceRight - roiRect.right); + float interY = Math.max(0f, roiRect.top - topY) + Math.max(0f, faceBottom - roiRect.bottom); + 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(Math.abs(faceBox.yaw) > SettingsActivity.getYawThreshold(context) || + Math.abs(faceBox.roll) > SettingsActivity.getRollThreshold(context) || + Math.abs(faceBox.pitch) > SettingsActivity.getPitchThreshold(context)) { + return FACE_CAPTURE_STATE.NO_FRONT; + } + + if(faceBox.face_occlusion > SettingsActivity.getOcclusionThreshold(context)) { + return FACE_CAPTURE_STATE.FACE_OCCLUDED; + } + + if(faceBox.left_eye_closed > SettingsActivity.getEyecloseThreshold(context) || + faceBox.right_eye_closed > SettingsActivity.getEyecloseThreshold(context)) { + return FACE_CAPTURE_STATE.EYE_CLOSED; + } + + if(faceBox.mouth_opened > SettingsActivity.getMouthopenThreshold(context)) { + return FACE_CAPTURE_STATE.MOUTH_OPENED; + } + + return FACE_CAPTURE_STATE.CAPTURE_OK; + } +} \ 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/DBManager.java b/app/src/main/java/com/kbyai/faceattribute/DBManager.java new file mode 100644 index 0000000..5510284 --- /dev/null +++ b/app/src/main/java/com/kbyai/faceattribute/DBManager.java @@ -0,0 +1,95 @@ +package com.kbyai.faceattribute; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; + +public class DBManager extends SQLiteOpenHelper { + + public static ArrayList personList = new ArrayList(); + + public DBManager(Context context) { + super(context, "mydb" , null, 1); + } + + @Override + public void onCreate(SQLiteDatabase db) { + // TODO Auto-generated method stub + db.execSQL( + "create table person " + + "(name text, face blob, templates blob)" + ); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // TODO Auto-generated method stub + db.execSQL("DROP TABLE IF EXISTS person"); + onCreate(db); + } + + public void insertPerson (String name, Bitmap face, byte[] templates) { + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + face.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream); + byte[] faceJpg = byteArrayOutputStream.toByteArray(); + + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues contentValues = new ContentValues(); + contentValues.put("name", name); + contentValues.put("face", faceJpg); + contentValues.put("templates", templates); + db.insert("person", null, contentValues); + + personList.add(new Person(name, face, templates)); + } + + public Integer deletePerson (String name) { + for(int i = 0; i < personList.size(); i ++) { + if(personList.get(i).name == name) { + personList.remove(i); + i --; + } + } + + SQLiteDatabase db = this.getWritableDatabase(); + return db.delete("person", + "name = ? ", + new String[] { name }); + } + + public Integer clearDB () { + personList.clear(); + + SQLiteDatabase db = this.getWritableDatabase(); + db.execSQL("delete from person"); + return 0; + } + + public void loadPerson() { + personList.clear(); + + SQLiteDatabase db = this.getReadableDatabase(); + Cursor res = db.rawQuery( "select * from person", null ); + res.moveToFirst(); + + while(res.isAfterLast() == false){ + String name = res.getString(res.getColumnIndex("name")); + byte[] faceJpg = res.getBlob(res.getColumnIndex("face")); + byte[] templates = res.getBlob(res.getColumnIndex("templates")); + Bitmap face = BitmapFactory.decodeByteArray(faceJpg, 0, faceJpg.length); + + Person person = new Person(name, face, templates); + personList.add(person); + + res.moveToNext(); + } + } +} \ No newline at end of file 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/FaceView.java b/app/src/main/java/com/kbyai/faceattribute/FaceView.java new file mode 100644 index 0000000..2bb09e0 --- /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 < SettingsActivity.getLivenessThreshold(context)) + { + 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/MainActivity.kt b/app/src/main/java/com/kbyai/faceattribute/MainActivity.kt new file mode 100644 index 0000000..bee2e83 --- /dev/null +++ b/app/src/main/java/com/kbyai/faceattribute/MainActivity.kt @@ -0,0 +1,175 @@ +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 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 dbManager: DBManager + private lateinit var textWarning: TextView + private lateinit var personAdapter: PersonAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + textWarning = findViewById(R.id.textWarning) + + + var ret = FaceSDK.setActivation( + "fGMbqRWAN9PrnQBHd3JtdbNCKJ75REHRN4yenuntm9SghMVrQztH8IQIObnN3hJc6RitR139CwnP\n" + + "P/hUVlINXCk48PkGrTJlNsFUm5ErOXL2QWw7IUzQow/DALUwvKOR4Qpz3i0lHKVlrFqMOKb4y3DH\n" + + "Dhb/Fh6KLywr5aWy5Lwv/hutFqe6gao9xVqpbOLq2yP+OIjPpW0teMxEjSKGhuQftp7lV9tEnv9B\n" + + "lAI75/ElCUYb6vxWCqZFSGLLiDuEyTbz7Npz1rhuQkwmotgLTYrij0zzIt79TccUve9lx2xl/fqS\n" + + "y6YUynuO4VN/awOJQFMv4HpFVFVupmU/ezM7Tg==" + ) + + 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!") + } + } + + dbManager = DBManager(this) + dbManager.loadPerson() + + personAdapter = PersonAdapter(this, DBManager.personList) + val listView: ListView = findViewById(R.id.listPerson) as ListView + listView.setAdapter(personAdapter) + + findViewById