diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..617b0ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.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..0afd8d5 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,54 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.kbyai.facerecognition' + compileSdk 33 + + defaultConfig { + applicationId "com.kbyai.facerecognition" + minSdk 24 + targetSdk 33 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + 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/facerecognition/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/kbyai/facerecognition/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..dcb62ab --- /dev/null +++ b/app/src/androidTest/java/com/kbyai/facerecognition/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.kbyai.facerecognition + +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.facerecognition", 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..2a25e13 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/kbyai/facerecognition/AboutActivity.kt b/app/src/main/java/com/kbyai/facerecognition/AboutActivity.kt new file mode 100644 index 0000000..665f9df --- /dev/null +++ b/app/src/main/java/com/kbyai/facerecognition/AboutActivity.kt @@ -0,0 +1,11 @@ +package com.kbyai.facerecognition + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle + +class AboutActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_about) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbyai/facerecognition/CameraActivity.java b/app/src/main/java/com/kbyai/facerecognition/CameraActivity.java new file mode 100644 index 0000000..c552240 --- /dev/null +++ b/app/src/main/java/com/kbyai/facerecognition/CameraActivity.java @@ -0,0 +1,264 @@ +package com.kbyai.facerecognition; + + +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.facesdk.FaceBox; +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); + + List faceBoxes = FaceSDK.faceDetection(bitmap); + + 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.similarityCalucation(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); + + 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/facerecognition/DBManager.java b/app/src/main/java/com/kbyai/facerecognition/DBManager.java new file mode 100644 index 0000000..a5acabf --- /dev/null +++ b/app/src/main/java/com/kbyai/facerecognition/DBManager.java @@ -0,0 +1,95 @@ +package com.kbyai.facerecognition; + +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/facerecognition/FaceView.java b/app/src/main/java/com/kbyai/facerecognition/FaceView.java new file mode 100644 index 0000000..66785a1 --- /dev/null +++ b/app/src/main/java/com/kbyai/facerecognition/FaceView.java @@ -0,0 +1,107 @@ +package com.kbyai.facerecognition; + +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.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/facerecognition/MainActivity.kt b/app/src/main/java/com/kbyai/facerecognition/MainActivity.kt new file mode 100644 index 0000000..24c484e --- /dev/null +++ b/app/src/main/java/com/kbyai/facerecognition/MainActivity.kt @@ -0,0 +1,119 @@ +package com.kbyai.facerecognition + +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Camera +import android.graphics.Rect +import android.os.Bundle +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.widget.* +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.preference.PreferenceManager +import com.kbyai.facesdk.FaceBox +import com.kbyai.facesdk.FaceSDK +import kotlin.random.Random + +class MainActivity : AppCompatActivity() { + + companion object { + private val SELECT_PHOTO_REQUEST_CODE = 1 + } + + 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( + "jfeIR/8sT+yQbR4FzN0fLYlSnJFHC/YDgncLJRHFFXp/7XN9ONJPcPUOWbj762y2jM0qq2QVFKkU\n" + + "Ae/vxlFXXcVFxxrz1EcQpnTf8tKdm978+s3GShWQgyWiE0mTHRDUa/7U9j0twL0pQ4X9STUA0LX3\n" + + "6xCgI57o6LcSUW0uWbCdG6xSz5dd41ko9jsPEVoysNLn8utabTcDsX+PadCVSBR5wpmMk0fyJJnD\n" + + "OCNv6YNgwE0IZKePQniqiGnG+Euw1hsmiCapuF7l+SnWYpWRGVaSQSak7NYW330JYQp0Rap2j4o4\n" + + "LfNHn9/oipn8BJCEFGS34WnznALHDdnuupjs2A==" + ) + + 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