diff --git a/.github/workflows/android-tests.yml b/.github/workflows/android-tests.yml index 49fcc84..30f9467 100644 --- a/.github/workflows/android-tests.yml +++ b/.github/workflows/android-tests.yml @@ -26,8 +26,17 @@ jobs: sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm + - name: Gradle cache + uses: gradle/actions/setup-gradle@v3 + - name: Run Tests uses: reactivecircus/android-emulator-runner@v2 with: api-level: 29 - script: ./gradlew connectedCheck + heap-size: 512M + script: | + # https://github.com/ReactiveCircus/android-emulator-runner/issues/264 + sleep 5 + $ANDROID_HOME/platform-tools/adb shell su root "setprop ctl.restart zygote" + sleep 10 + ./gradlew connectedCheck diff --git a/build.gradle.kts b/build.gradle.kts index 3d60de0..e7dbe7c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,9 +32,6 @@ android { kotlinOptions { jvmTarget = "17" } - buildFeatures { - mlModelBinding = true - } } dependencies { diff --git a/src/androidTest/java/com/simprints/simface/CustomModelTest.kt b/src/androidTest/java/com/simprints/simface/CustomModelTest.kt new file mode 100644 index 0000000..714b5b1 --- /dev/null +++ b/src/androidTest/java/com/simprints/simface/CustomModelTest.kt @@ -0,0 +1,59 @@ +package com.simprints.simface + +import android.content.Context +import android.graphics.Bitmap +import androidx.test.core.app.* +import com.simprints.simface.core.SimFace +import com.simprints.simface.core.SimFaceConfig +import com.simprints.simface.core.Utils +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertArrayEquals +import org.junit.Before +import org.junit.Test + +/** + * This test class makes it trivially easy to run tests with new model files: + * 1. Add the new model file to the anrdroidTest/res/raw folder + * 2. Create a new test method + * 3. Provide the model file name to `openTestModelFile()` + * 4. Do the testing + */ +class CustomModelTest { + private lateinit var simFace: SimFace + private lateinit var context: Context + + @Before + fun setup() { + context = ApplicationProvider.getApplicationContext() + simFace = SimFace() + } + + @After + fun cleanup() { + simFace.release() + } + + @Test + fun test_processes_face_with_custom_model() = runTest { + val testModelFile = context.openTestModelFile() + + simFace.initialize( + SimFaceConfig( + context, + customModel = SimFaceConfig.CustomModel( + file = testModelFile, + templateVersion = "TEST_1", + ), + ), + ) + val bitmap: Bitmap = context.loadBitmapFromTestResources("royalty_free_good_face") + val resultFloat = getFaceEmbeddingFromBitmap(bitmap) + + assertArrayEquals(GOOD_FACE_EMBEDDING, resultFloat, 0.1F) + } + + private fun getFaceEmbeddingFromBitmap(bitmap: Bitmap): FloatArray = simFace + .getEmbedding(bitmap) + .let { Utils.byteArrayToFloatArray(it) } +} diff --git a/src/androidTest/java/com/simprints/simface/EmbeddingProcessorTest.kt b/src/androidTest/java/com/simprints/simface/EmbeddingProcessorTest.kt index 832a53e..452feb3 100644 --- a/src/androidTest/java/com/simprints/simface/EmbeddingProcessorTest.kt +++ b/src/androidTest/java/com/simprints/simface/EmbeddingProcessorTest.kt @@ -7,6 +7,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.simprints.simface.core.SimFace import com.simprints.simface.core.SimFaceConfig import com.simprints.simface.core.Utils +import org.junit.After import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertTrue import org.junit.Before @@ -25,6 +26,11 @@ class EmbeddingProcessorTest { simFace.initialize(SimFaceConfig(context)) } + @After + fun cleanup() { + simFace.release() + } + @Test fun get_embedding_with_image() { val bitmap: Bitmap = context.loadBitmapFromTestResources("royalty_free_good_face") @@ -33,69 +39,8 @@ class EmbeddingProcessorTest { assertTrue(Utils.byteArrayToFloatArray(result).size == 512) - // Define the expected output for our image (the output is computed on Android) - val expectedEmbedding = floatArrayOf( - 0.1135F, -0.1753F, 0.0677F, -0.0124F, 0.1674F, -0.0951F, 0.0191F, -0.2586F, 0.091F, - -0.2461F, 0.0696F, 0.0089F, 0.0044F, -0.0732F, 0.0606F, 0.037F, -0.1594F, 0.268F, - -0.1401F, 0.1506F, -0.2159F, -0.0905F, 0.087F, -0.1309F, -0.0511F, 0.1077F, 0.0931F, - 0.0872F, 0.086F, 0.1203F, -0.1676F, 0.0235F, -0.2806F, -0.0329F, 0.1529F, 0.1186F, - 0.03F, 0.0686F, -0.0713F, -0.1314F, -0.0986F, -0.0724F, 0.1329F, 0.0092F, -0.1193F, - -0.0391F, -0.1797F, -0.127F, 0.2789F, 0.0851F, 0.078F, 0.1067F, 0.0375F, 0.1518F, - 0.0008F, 0.1018F, 0.1809F, 0.1209F, -0.1117F, -0.1472F, 0.0554F, 0.0977F, -0.1013F, - -0.0611F, 0.0388F, 0.2212F, 0.0033F, 0.0463F, 0.0278F, 0.0485F, -0.1307F, -0.0365F, - -0.0826F, 0.0405F, -0.0073F, 0.299F, 0.1135F, -0.0102F, -0.0982F, -0.1587F, -0.1466F, - 0.0591F, -0.0091F, 0.0691F, -0.0868F, -0.0896F, -0.0628F, -0.0852F, -0.0948F, 0.0316F, - -0.0861F, -0.1777F, -0.3523F, -0.186F, -0.1411F, 0.012F, -0.1373F, 0.1749F, -0.0249F, - 0.0509F, 0.0131F, 0.1686F, 0.0551F, 0.2373F, -0.064F, -0.028F, -0.1848F, -0.0349F, - 0.2469F, 0.0965F, -0.1407F, 0.0004F, 0.0209F, -0.0247F, -0.0216F, 0.0652F, -0.0333F, - 0.0948F, -0.0806F, 0.1441F, -0.117F, -0.1104F, 0.07F, -0.0372F, -0.0341F, 0.1117F, - 0.0481F, -0.2373F, 0.135F, 0.014F, -0.0972F, -0.0469F, 0.0211F, -0.1202F, 0.0437F, - 0.0257F, 0.1639F, 0.0143F, 0.0503F, -0.0142F, 0.0327F, 0.0882F, 0.0063F, -0.0172F, - 0.0412F, -0.1465F, 0.22F, 0.0429F, -0.1867F, 0.1597F, -0.1326F, 0.0647F, 0.3016F, - -0.0428F, -0.2369F, -0.0455F, -0.0397F, -0.1079F, 0.1862F, -0.0116F, 0.0553F, 0.1248F, - 0.131F, 0.0128F, -0.0781F, 0.0971F, 0.0904F, -0.0411F, 0.0961F, -0.1152F, 0.199F, - 0.1153F, -0.3224F, -0.1733F, 0.05F, -0.0446F, -0.1369F, 0.1701F, 0.2333F, -0.2317F, - 0.002F, -0.0351F, 0.0046F, 0.1207F, -0.2001F, -0.0382F, -0.0422F, 0.1825F, -0.0938F, - 0.2165F, 0.0996F, 0.1071F, 0.0128F, 0.1434F, -0.1021F, -0.1902F, -0.0408F, -0.0902F, - -0.031F, -0.0502F, -0.0982F, -0.0567F, 0.1543F, 0.1186F, -0.0727F, -0.0838F, -0.0971F, - -0.1439F, -0.2429F, -0.0308F, 0.1349F, 0.0538F, 0.0568F, -0.2891F, 0.0614F, 0.1271F, - -0.1079F, 0.074F, -0.0999F, -0.1479F, -0.0597F, -0.2288F, 0.1506F, 0.1161F, -0.0138F, - -0.1488F, -0.0501F, 0.0919F, -0.0453F, -0.0628F, -0.059F, 0.1338F, 0.155F, 0.0091F, - 0.0771F, 0.0666F, 0.0869F, 0.0258F, -0.0684F, 0.0951F, 0.0452F, -0.2022F, 0.1382F, - 0.0733F, 0.055F, 0.0729F, 0.0788F, -0.2598F, 0.0132F, 0.114F, -0.0869F, -0.1626F, - 0.0236F, 0.0724F, 0.0425F, -0.0393F, 0.1494F, -0.0671F, -0.0336F, -0.0595F, -0.1619F, - 0.0663F, -0.08F, -0.205F, -0.003F, 0.0969F, 0.1377F, 0.0062F, -0.0457F, -0.126F, - 0.0655F, -0.0487F, -0.0257F, 0.0424F, 0.2309F, -0.099F, 0.0163F, -0.0458F, -0.0571F, - -0.0574F, 0.0281F, 0.1171F, -0.1953F, -0.0976F, -0.05F, -0.2563F, -0.0281F, -0.0871F, - 0.3235F, 0.0788F, 0.2908F, 0.1366F, 0.0607F, -0.0818F, -0.0054F, -0.0376F, -0.1022F, - 0.0616F, -0.1894F, -0.0358F, 0.2847F, 0.0595F, -0.1124F, 0.1173F, 0.174F, 0.0755F, - 0.0573F, -0.0637F, -0.0562F, -0.135F, -0.0006F, -0.1519F, -0.1022F, 0.1712F, 0.0848F, - 0.0547F, 0.0366F, -0.0063F, -0.0713F, -0.0349F, -0.0271F, -0.1853F, -0.1445F, 0.0208F, - 0.0398F, 0.1813F, 0.0798F, -0.0562F, -0.1128F, -0.0147F, 0.1764F, -0.0833F, 0.1118F, - -0.2129F, -0.1F, -0.1167F, -0.2579F, 0.0198F, 0.0885F, -0.095F, -0.0838F, -0.144F, - 0.0299F, -0.0591F, -0.102F, 0.049F, 0.0343F, 0.1298F, 0.0956F, -0.0458F, -0.0474F, - 0.0315F, 0.1484F, 0.2539F, -0.0043F, -0.186F, -0.2215F, 0.1075F, 0.1652F, 0.2939F, - 0.0095F, -0.0881F, 0.1055F, -0.1063F, 0.0289F, -0.1739F, 0.0477F, -0.1992F, -0.1366F, - -0.0555F, 0.0207F, -0.0222F, -0.0415F, -0.1433F, -0.0271F, 0.0524F, -0.1316F, 0.2686F, - 0.1246F, 0.2177F, 0.1663F, -0.1196F, -0.0851F, -0.1035F, 0.0225F, 0.0033F, -0.1908F, - 0.1418F, 0.1604F, -0.1515F, -0.1319F, 0.0213F, -0.2297F, -0.0265F, -0.081F, 0.0559F, - 0.1326F, -0.1169F, 0.0283F, 0.0001F, 0.0463F, -0.0764F, 0.0277F, 0.1246F, -0.1428F, - -0.1475F, 0.0022F, 0.1023F, -0.0439F, 0.0696F, 0.0047F, -0.1234F, -0.0703F, -0.0483F, - 0.0474F, 0.2345F, 0.0725F, 0.1313F, -0.1151F, -0.0591F, -0.2275F, -0.2104F, 0.0691F, - -0.0486F, -0.059F, -0.2237F, -0.1017F, 0.0346F, 0.1812F, -0.0554F, 0.1307F, 0.1192F, - 0.1939F, 0.1052F, 0.0822F, -0.0595F, -0.1773F, 0.1384F, -0.1298F, -0.1249F, -0.0362F, - -0.1397F, 0.0448F, 0.116F, 0.1894F, 0.0727F, -0.2113F, 0.0221F, -0.0064F, 0.0144F, - 0.09F, 0.0875F, -0.1423F, 0.127F, -0.0044F, 0.0062F, 0.1248F, 0.0782F, 0.2054F, - 0.1255F, 0.0374F, -0.0638F, -0.0931F, -0.024F, -0.2686F, -0.1965F, 0.0769F, -0.1417F, - 0.06F, 0.0964F, -0.0566F, -0.2128F, -0.0131F, 0.1782F, 0.019F, 0.0467F, 0.0065F, - 0.1045F, -0.1654F, 0.0802F, 0.1897F, 0.1411F, -0.0211F, 0.0146F, -0.0734F, 0.0369F, - 0.0555F, 0.0482F, 0.0454F, -0.1672F, 0.007F, 0.0741F, 0.0571F, 0.1477F, 0.0634F, - 0.0808F, -0.1018F, 0.0234F, -0.1357F, 0.0398F, 0.0549F, 0.1207F, 0.1144F, -0.1265F, - 0.0115F, 0.1952F, -0.0571F, -0.0351F, -0.0523F, -0.1019F, 0.0142F, 0.0551F, - ) - // Verify results - assertArrayEquals(expectedEmbedding, resultFloat, 0.1F) + assertArrayEquals(GOOD_FACE_EMBEDDING, resultFloat, 0.1F) } @Test diff --git a/src/androidTest/java/com/simprints/simface/ExpectedEmbedding.kt b/src/androidTest/java/com/simprints/simface/ExpectedEmbedding.kt new file mode 100644 index 0000000..228adfa --- /dev/null +++ b/src/androidTest/java/com/simprints/simface/ExpectedEmbedding.kt @@ -0,0 +1,517 @@ +package com.simprints.simface + +// Define the expected output for our image (the output is computed on Android) +val GOOD_FACE_EMBEDDING = floatArrayOf( + 0.1135F, + -0.1753F, + 0.0677F, + -0.0124F, + 0.1674F, + -0.0951F, + 0.0191F, + -0.2586F, + 0.091F, + -0.2461F, + 0.0696F, + 0.0089F, + 0.0044F, + -0.0732F, + 0.0606F, + 0.037F, + -0.1594F, + 0.268F, + -0.1401F, + 0.1506F, + -0.2159F, + -0.0905F, + 0.087F, + -0.1309F, + -0.0511F, + 0.1077F, + 0.0931F, + 0.0872F, + 0.086F, + 0.1203F, + -0.1676F, + 0.0235F, + -0.2806F, + -0.0329F, + 0.1529F, + 0.1186F, + 0.03F, + 0.0686F, + -0.0713F, + -0.1314F, + -0.0986F, + -0.0724F, + 0.1329F, + 0.0092F, + -0.1193F, + -0.0391F, + -0.1797F, + -0.127F, + 0.2789F, + 0.0851F, + 0.078F, + 0.1067F, + 0.0375F, + 0.1518F, + 0.0008F, + 0.1018F, + 0.1809F, + 0.1209F, + -0.1117F, + -0.1472F, + 0.0554F, + 0.0977F, + -0.1013F, + -0.0611F, + 0.0388F, + 0.2212F, + 0.0033F, + 0.0463F, + 0.0278F, + 0.0485F, + -0.1307F, + -0.0365F, + -0.0826F, + 0.0405F, + -0.0073F, + 0.299F, + 0.1135F, + -0.0102F, + -0.0982F, + -0.1587F, + -0.1466F, + 0.0591F, + -0.0091F, + 0.0691F, + -0.0868F, + -0.0896F, + -0.0628F, + -0.0852F, + -0.0948F, + 0.0316F, + -0.0861F, + -0.1777F, + -0.3523F, + -0.186F, + -0.1411F, + 0.012F, + -0.1373F, + 0.1749F, + -0.0249F, + 0.0509F, + 0.0131F, + 0.1686F, + 0.0551F, + 0.2373F, + -0.064F, + -0.028F, + -0.1848F, + -0.0349F, + 0.2469F, + 0.0965F, + -0.1407F, + 0.0004F, + 0.0209F, + -0.0247F, + -0.0216F, + 0.0652F, + -0.0333F, + 0.0948F, + -0.0806F, + 0.1441F, + -0.117F, + -0.1104F, + 0.07F, + -0.0372F, + -0.0341F, + 0.1117F, + 0.0481F, + -0.2373F, + 0.135F, + 0.014F, + -0.0972F, + -0.0469F, + 0.0211F, + -0.1202F, + 0.0437F, + 0.0257F, + 0.1639F, + 0.0143F, + 0.0503F, + -0.0142F, + 0.0327F, + 0.0882F, + 0.0063F, + -0.0172F, + 0.0412F, + -0.1465F, + 0.22F, + 0.0429F, + -0.1867F, + 0.1597F, + -0.1326F, + 0.0647F, + 0.3016F, + -0.0428F, + -0.2369F, + -0.0455F, + -0.0397F, + -0.1079F, + 0.1862F, + -0.0116F, + 0.0553F, + 0.1248F, + 0.131F, + 0.0128F, + -0.0781F, + 0.0971F, + 0.0904F, + -0.0411F, + 0.0961F, + -0.1152F, + 0.199F, + 0.1153F, + -0.3224F, + -0.1733F, + 0.05F, + -0.0446F, + -0.1369F, + 0.1701F, + 0.2333F, + -0.2317F, + 0.002F, + -0.0351F, + 0.0046F, + 0.1207F, + -0.2001F, + -0.0382F, + -0.0422F, + 0.1825F, + -0.0938F, + 0.2165F, + 0.0996F, + 0.1071F, + 0.0128F, + 0.1434F, + -0.1021F, + -0.1902F, + -0.0408F, + -0.0902F, + -0.031F, + -0.0502F, + -0.0982F, + -0.0567F, + 0.1543F, + 0.1186F, + -0.0727F, + -0.0838F, + -0.0971F, + -0.1439F, + -0.2429F, + -0.0308F, + 0.1349F, + 0.0538F, + 0.0568F, + -0.2891F, + 0.0614F, + 0.1271F, + -0.1079F, + 0.074F, + -0.0999F, + -0.1479F, + -0.0597F, + -0.2288F, + 0.1506F, + 0.1161F, + -0.0138F, + -0.1488F, + -0.0501F, + 0.0919F, + -0.0453F, + -0.0628F, + -0.059F, + 0.1338F, + 0.155F, + 0.0091F, + 0.0771F, + 0.0666F, + 0.0869F, + 0.0258F, + -0.0684F, + 0.0951F, + 0.0452F, + -0.2022F, + 0.1382F, + 0.0733F, + 0.055F, + 0.0729F, + 0.0788F, + -0.2598F, + 0.0132F, + 0.114F, + -0.0869F, + -0.1626F, + 0.0236F, + 0.0724F, + 0.0425F, + -0.0393F, + 0.1494F, + -0.0671F, + -0.0336F, + -0.0595F, + -0.1619F, + 0.0663F, + -0.08F, + -0.205F, + -0.003F, + 0.0969F, + 0.1377F, + 0.0062F, + -0.0457F, + -0.126F, + 0.0655F, + -0.0487F, + -0.0257F, + 0.0424F, + 0.2309F, + -0.099F, + 0.0163F, + -0.0458F, + -0.0571F, + -0.0574F, + 0.0281F, + 0.1171F, + -0.1953F, + -0.0976F, + -0.05F, + -0.2563F, + -0.0281F, + -0.0871F, + 0.3235F, + 0.0788F, + 0.2908F, + 0.1366F, + 0.0607F, + -0.0818F, + -0.0054F, + -0.0376F, + -0.1022F, + 0.0616F, + -0.1894F, + -0.0358F, + 0.2847F, + 0.0595F, + -0.1124F, + 0.1173F, + 0.174F, + 0.0755F, + 0.0573F, + -0.0637F, + -0.0562F, + -0.135F, + -0.0006F, + -0.1519F, + -0.1022F, + 0.1712F, + 0.0848F, + 0.0547F, + 0.0366F, + -0.0063F, + -0.0713F, + -0.0349F, + -0.0271F, + -0.1853F, + -0.1445F, + 0.0208F, + 0.0398F, + 0.1813F, + 0.0798F, + -0.0562F, + -0.1128F, + -0.0147F, + 0.1764F, + -0.0833F, + 0.1118F, + -0.2129F, + -0.1F, + -0.1167F, + -0.2579F, + 0.0198F, + 0.0885F, + -0.095F, + -0.0838F, + -0.144F, + 0.0299F, + -0.0591F, + -0.102F, + 0.049F, + 0.0343F, + 0.1298F, + 0.0956F, + -0.0458F, + -0.0474F, + 0.0315F, + 0.1484F, + 0.2539F, + -0.0043F, + -0.186F, + -0.2215F, + 0.1075F, + 0.1652F, + 0.2939F, + 0.0095F, + -0.0881F, + 0.1055F, + -0.1063F, + 0.0289F, + -0.1739F, + 0.0477F, + -0.1992F, + -0.1366F, + -0.0555F, + 0.0207F, + -0.0222F, + -0.0415F, + -0.1433F, + -0.0271F, + 0.0524F, + -0.1316F, + 0.2686F, + 0.1246F, + 0.2177F, + 0.1663F, + -0.1196F, + -0.0851F, + -0.1035F, + 0.0225F, + 0.0033F, + -0.1908F, + 0.1418F, + 0.1604F, + -0.1515F, + -0.1319F, + 0.0213F, + -0.2297F, + -0.0265F, + -0.081F, + 0.0559F, + 0.1326F, + -0.1169F, + 0.0283F, + 0.0001F, + 0.0463F, + -0.0764F, + 0.0277F, + 0.1246F, + -0.1428F, + -0.1475F, + 0.0022F, + 0.1023F, + -0.0439F, + 0.0696F, + 0.0047F, + -0.1234F, + -0.0703F, + -0.0483F, + 0.0474F, + 0.2345F, + 0.0725F, + 0.1313F, + -0.1151F, + -0.0591F, + -0.2275F, + -0.2104F, + 0.0691F, + -0.0486F, + -0.059F, + -0.2237F, + -0.1017F, + 0.0346F, + 0.1812F, + -0.0554F, + 0.1307F, + 0.1192F, + 0.1939F, + 0.1052F, + 0.0822F, + -0.0595F, + -0.1773F, + 0.1384F, + -0.1298F, + -0.1249F, + -0.0362F, + -0.1397F, + 0.0448F, + 0.116F, + 0.1894F, + 0.0727F, + -0.2113F, + 0.0221F, + -0.0064F, + 0.0144F, + 0.09F, + 0.0875F, + -0.1423F, + 0.127F, + -0.0044F, + 0.0062F, + 0.1248F, + 0.0782F, + 0.2054F, + 0.1255F, + 0.0374F, + -0.0638F, + -0.0931F, + -0.024F, + -0.2686F, + -0.1965F, + 0.0769F, + -0.1417F, + 0.06F, + 0.0964F, + -0.0566F, + -0.2128F, + -0.0131F, + 0.1782F, + 0.019F, + 0.0467F, + 0.0065F, + 0.1045F, + -0.1654F, + 0.0802F, + 0.1897F, + 0.1411F, + -0.0211F, + 0.0146F, + -0.0734F, + 0.0369F, + 0.0555F, + 0.0482F, + 0.0454F, + -0.1672F, + 0.007F, + 0.0741F, + 0.0571F, + 0.1477F, + 0.0634F, + 0.0808F, + -0.1018F, + 0.0234F, + -0.1357F, + 0.0398F, + 0.0549F, + 0.1207F, + 0.1144F, + -0.1265F, + 0.0115F, + 0.1952F, + -0.0571F, + -0.0351F, + -0.0523F, + -0.1019F, + 0.0142F, + 0.0551F, +) diff --git a/src/androidTest/java/com/simprints/simface/FaceAlignTest.kt b/src/androidTest/java/com/simprints/simface/FaceAlignTest.kt index 582c87b..ce9eef1 100644 --- a/src/androidTest/java/com/simprints/simface/FaceAlignTest.kt +++ b/src/androidTest/java/com/simprints/simface/FaceAlignTest.kt @@ -12,6 +12,7 @@ import com.simprints.simface.quality.cropAlignFace import com.simprints.simface.quality.warpAlignFace import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -27,6 +28,11 @@ class FaceAlignTest { simFace.initialize(SimFaceConfig(context)) } + @After + fun cleanup() { + simFace.release() + } + @Test fun crop_image_with_valid_bounding_box() { val bitmap: Bitmap = context.loadBitmapFromTestResources("royalty_free_flower") diff --git a/src/androidTest/java/com/simprints/simface/FaceDetectionProcessorTest.kt b/src/androidTest/java/com/simprints/simface/FaceDetectionProcessorTest.kt index fe0762f..7b99858 100644 --- a/src/androidTest/java/com/simprints/simface/FaceDetectionProcessorTest.kt +++ b/src/androidTest/java/com/simprints/simface/FaceDetectionProcessorTest.kt @@ -9,6 +9,7 @@ import com.simprints.simface.core.SimFaceConfig import com.simprints.simface.data.FaceDetection import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -26,6 +27,11 @@ class FaceDetectionProcessorTest { simFace.initialize(SimFaceConfig(context)) } + @After + fun cleanup() { + simFace.release() + } + @Test fun normal_image_gets_high_score() = runTest { val bitmap: Bitmap = context.loadBitmapFromTestResources("royalty_free_good_face") diff --git a/src/androidTest/java/com/simprints/simface/IdentificationTest.kt b/src/androidTest/java/com/simprints/simface/IdentificationTest.kt index 527c7b6..a6386e5 100644 --- a/src/androidTest/java/com/simprints/simface/IdentificationTest.kt +++ b/src/androidTest/java/com/simprints/simface/IdentificationTest.kt @@ -6,6 +6,7 @@ import androidx.test.ext.junit.runners.* import com.simprints.simface.core.SimFace import com.simprints.simface.core.SimFaceConfig import com.simprints.simface.core.Utils +import org.junit.After import org.junit.Assert import org.junit.Before import org.junit.Test @@ -22,6 +23,11 @@ class IdentificationTest { simFace.initialize(SimFaceConfig(context)) } + @After + fun cleanup() { + simFace.release() + } + @Test fun score_map_should_be_ordered_by_distance() { val referenceArray = Utils.floatArrayToByteArray(floatArrayOf(1.0f, 0.0f)) diff --git a/src/androidTest/java/com/simprints/simface/TestContextExt.kt b/src/androidTest/java/com/simprints/simface/TestContextExt.kt index 6e4f6a4..9bd1732 100644 --- a/src/androidTest/java/com/simprints/simface/TestContextExt.kt +++ b/src/androidTest/java/com/simprints/simface/TestContextExt.kt @@ -3,6 +3,9 @@ package com.simprints.simface import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream /** * Resources IDs are not available via the IDE tooling and therefore to run @@ -16,3 +19,15 @@ internal fun Context.loadBitmapFromTestResources(drawableName: String): Bitmap { return BitmapFactory.decodeResource(resources, resourceId) ?: throw IllegalStateException("BitmapFactory failed to decode resource '$drawableName'") } + +internal fun Context.openTestModelFile(resourceName: String = "edgeface_test"): File { + val resourceId = resources.getIdentifier(resourceName, "raw", packageName) + if (resourceId == 0) { + throw IllegalStateException("Test resource '$resourceName' not found in package '$packageName'") + } + + val inputStream: InputStream = resources.openRawResource(resourceId) + val tempFile = File.createTempFile("test_model", ".tflite", cacheDir) + FileOutputStream(tempFile).use { outputStream -> inputStream.use { input -> input.copyTo(outputStream) } } + return tempFile +} diff --git a/src/androidTest/java/com/simprints/simface/VerificationTest.kt b/src/androidTest/java/com/simprints/simface/VerificationTest.kt index cf79697..787dcd5 100644 --- a/src/androidTest/java/com/simprints/simface/VerificationTest.kt +++ b/src/androidTest/java/com/simprints/simface/VerificationTest.kt @@ -6,6 +6,7 @@ import androidx.test.ext.junit.runners.* import com.simprints.simface.core.SimFace import com.simprints.simface.core.SimFaceConfig import com.simprints.simface.core.Utils +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before @@ -23,6 +24,11 @@ class VerificationTest { simFace.initialize(SimFaceConfig(context)) } + @After + fun cleanup() { + simFace.release() + } + @Test fun score_between_identical_vectors_should_be_one() { val array1 = Utils.floatArrayToByteArray(floatArrayOf(1.0f, 0.0f, 0.0f)) diff --git a/src/main/ml/edgeface_s_gamma_05.tflite b/src/androidTest/res/raw/edgeface_test.tflite similarity index 100% rename from src/main/ml/edgeface_s_gamma_05.tflite rename to src/androidTest/res/raw/edgeface_test.tflite diff --git a/src/main/assets/edgeface_s_gamma_05.tflite b/src/main/assets/edgeface_s_gamma_05.tflite new file mode 100644 index 0000000..53c3fed Binary files /dev/null and b/src/main/assets/edgeface_s_gamma_05.tflite differ diff --git a/src/main/java/com/simprints/simface/core/MLModelManager.kt b/src/main/java/com/simprints/simface/core/MLModelManager.kt deleted file mode 100644 index 2281178..0000000 --- a/src/main/java/com/simprints/simface/core/MLModelManager.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.simprints.simface.core - -import android.content.Context -import com.simprints.biometrics.simface.ml.EdgefaceSGamma05 - -internal class MLModelManager { - private lateinit var faceEmbeddingModel: EdgefaceSGamma05 - - fun loadModels(context: Context) { - // Load Face Embedding Model - faceEmbeddingModel = EdgefaceSGamma05.newInstance(context) - } - - fun getFaceEmbeddingModel(): EdgefaceSGamma05 = faceEmbeddingModel - - fun close() { - faceEmbeddingModel.close() - } -} diff --git a/src/main/java/com/simprints/simface/core/SimFace.kt b/src/main/java/com/simprints/simface/core/SimFace.kt index 9647ec3..901b996 100644 --- a/src/main/java/com/simprints/simface/core/SimFace.kt +++ b/src/main/java/com/simprints/simface/core/SimFace.kt @@ -5,6 +5,7 @@ import com.google.mlkit.vision.face.FaceDetector import com.google.mlkit.vision.face.FaceDetectorOptions import com.simprints.simface.data.FaceDetection import com.simprints.simface.embedding.EmbeddingProcessor +import com.simprints.simface.embedding.MLModelManager import com.simprints.simface.embedding.TensorFlowEmbeddingProcessor import com.simprints.simface.matcher.CosineDistanceMatchProcessor import com.simprints.simface.matcher.MatchProcessor @@ -29,8 +30,7 @@ class SimFace { fun initialize(config: SimFaceConfig): Unit = synchronized(initLock) { try { // Initialize the model manager with the given config - modelManager = MLModelManager() - modelManager.loadModels(config.context) + modelManager = MLModelManager(config) // Initialize processors embeddingProcessor = TensorFlowEmbeddingProcessor(modelManager) @@ -71,7 +71,7 @@ class SimFace { /** * Returns the version of the templates generated by the underlying ML model. */ - fun getTemplateVersion(): String = TEMPLATE_VERSION + fun getTemplateVersion(): String = modelManager.templateVersion /** * Asynchronously processes the image and finds a face on the provided image and returns @@ -136,8 +136,4 @@ class SimFace { } return matchProcessor.identificationScore(probe, matchReferences) } - - companion object { - private const val TEMPLATE_VERSION = "SIM_FACE_BASE_1" - } } diff --git a/src/main/java/com/simprints/simface/core/SimFaceConfig.kt b/src/main/java/com/simprints/simface/core/SimFaceConfig.kt index cd181ad..ae724e5 100644 --- a/src/main/java/com/simprints/simface/core/SimFaceConfig.kt +++ b/src/main/java/com/simprints/simface/core/SimFaceConfig.kt @@ -1,7 +1,18 @@ package com.simprints.simface.core import android.content.Context +import java.io.File data class SimFaceConfig( - val context: Context, -) + val applicationContext: Context, + /** + * Custom model file to use instead of the bundled one. If not set, the bundled model will be used. + * The custom model's inputs and outputs vectors must much the default SimFace model. + */ + val customModel: CustomModel? = null, +) { + data class CustomModel( + val file: File, + val templateVersion: String, + ) +} diff --git a/src/main/java/com/simprints/simface/core/Utils.kt b/src/main/java/com/simprints/simface/core/Utils.kt index 74ca8ca..82e7353 100644 --- a/src/main/java/com/simprints/simface/core/Utils.kt +++ b/src/main/java/com/simprints/simface/core/Utils.kt @@ -1,11 +1,14 @@ package com.simprints.simface.core +import android.graphics.Bitmap import android.graphics.Rect import java.nio.ByteBuffer import java.nio.ByteOrder internal object Utils { const val IMAGE_SIZE = 112 + const val OUTPUT_EMBEDDING_SIZE = 512 + const val DEFAULT_TEMPLATE_VERSION = "SIM_FACE_BASE_1" /** * Converts a FloatArray to a ByteArray. @@ -48,4 +51,41 @@ internal object Utils { right.coerceAtMost(width), bottom.coerceAtMost(height), ) + + /** + * Convert image into a 1D array of pixel color values in RGB order. + */ + internal fun Bitmap.toIntArray(imageSize: Int): IntArray { + val intValues = IntArray(imageSize * imageSize) + val resultArray = IntArray(imageSize * imageSize * 3) + + getPixels(intValues, 0, imageSize, 0, 0, imageSize, imageSize) + + var index = 0 + for (pixel in intValues) { + resultArray[index++] = (pixel shr 16) and 255 // Red + resultArray[index++] = (pixel shr 8) and 255 // Green + resultArray[index++] = pixel and 255 // Blue + } + return resultArray + } + + /** + * Convert image into a 1D array of pixel color + * values in RGB order in [0,1] range. + */ + internal fun Bitmap.toFloatArray(imageSize: Int): FloatArray { + val intValues = IntArray(imageSize * imageSize) + val resultArray = FloatArray(imageSize * imageSize * 3) + + getPixels(intValues, 0, imageSize, 0, 0, imageSize, imageSize) + + var index = 0 + for (pixel in intValues) { + resultArray[index++] = ((pixel shr 16) and 255) / 255f // Red + resultArray[index++] = ((pixel shr 8) and 255) / 255f // Green + resultArray[index++] = (pixel and 255) / 255f // Blue + } + return resultArray + } } diff --git a/src/main/java/com/simprints/simface/embedding/MLModelManager.kt b/src/main/java/com/simprints/simface/embedding/MLModelManager.kt new file mode 100644 index 0000000..5eacd54 --- /dev/null +++ b/src/main/java/com/simprints/simface/embedding/MLModelManager.kt @@ -0,0 +1,53 @@ +package com.simprints.simface.embedding + +import com.simprints.simface.core.SimFaceConfig +import com.simprints.simface.core.Utils +import org.tensorflow.lite.Interpreter +import java.nio.ByteBuffer +import java.nio.channels.Channels +import java.nio.channels.FileChannel + +internal class MLModelManager( + config: SimFaceConfig, +) { + private val interpreter: Interpreter + + val templateVersion: String = config.customModel?.templateVersion ?: Utils.DEFAULT_TEMPLATE_VERSION + + init { + + val modelFileByteBuffer = if (config.customModel == null) { + loadDefaultModelFile(config) + } else { + config.customModel.file.inputStream().channel.use { fileChannel -> + fileChannel.map(FileChannel.MapMode.READ_ONLY, 0L, fileChannel.size()) + } + } + val options = Interpreter.Options().apply { + setNumThreads(1) + } + + interpreter = Interpreter(modelFileByteBuffer, options) + } + + fun getInterpreter(): Interpreter = interpreter + + fun close() { + interpreter.close() + } + + private fun loadDefaultModelFile(config: SimFaceConfig): ByteBuffer = config.applicationContext + .assets + .open(DEFAULT_MODEL_FILENAME) + .use { inputStream -> + val size = inputStream.available() + val buffer = ByteBuffer.allocateDirect(size) // Use allocateDirect for native libs + Channels.newChannel(inputStream).use { channel -> channel.read(buffer) } + buffer.flip() + buffer + } + + companion object { + private const val DEFAULT_MODEL_FILENAME = "edgeface_s_gamma_05.tflite" + } +} diff --git a/src/main/java/com/simprints/simface/embedding/TensorFlowEmbeddingProcessor.kt b/src/main/java/com/simprints/simface/embedding/TensorFlowEmbeddingProcessor.kt index f1c4a54..4dbcba5 100644 --- a/src/main/java/com/simprints/simface/embedding/TensorFlowEmbeddingProcessor.kt +++ b/src/main/java/com/simprints/simface/embedding/TensorFlowEmbeddingProcessor.kt @@ -2,26 +2,23 @@ package com.simprints.simface.embedding import android.graphics.Bitmap import androidx.core.graphics.scale -import com.simprints.simface.core.MLModelManager +import com.simprints.simface.embedding.MLModelManager import com.simprints.simface.core.Utils import com.simprints.simface.core.Utils.IMAGE_SIZE +import com.simprints.simface.core.Utils.OUTPUT_EMBEDDING_SIZE +import com.simprints.simface.core.Utils.toFloatArray import org.tensorflow.lite.DataType +import org.tensorflow.lite.Interpreter import org.tensorflow.lite.support.common.TensorProcessor import org.tensorflow.lite.support.common.ops.NormalizeOp import org.tensorflow.lite.support.tensorbuffer.TensorBuffer internal class TensorFlowEmbeddingProcessor( - private val modelManager: MLModelManager, + modelManager: MLModelManager, ) : EmbeddingProcessor { - override fun getEmbedding(bitmap: Bitmap): ByteArray { - val imageTensorProcessor = TensorProcessor - .Builder() - .add(NormalizeOp(0.5f, 0.5f)) - .add(ReshapeOp()) - .build() - - val model = modelManager.getFaceEmbeddingModel() + private val interpreter: Interpreter = modelManager.getInterpreter() + override fun getEmbedding(bitmap: Bitmap): ByteArray { val resizedBitmap = if (bitmap.height != IMAGE_SIZE || bitmap.width != IMAGE_SIZE) { try { bitmap.scale(IMAGE_SIZE, IMAGE_SIZE, false) @@ -32,39 +29,26 @@ internal class TensorFlowEmbeddingProcessor( bitmap } - val inputBuffer = resizedBitmap.toIntArray(IMAGE_SIZE) - val floatBuffer = inputBuffer.map { it / 255f }.toFloatArray() - - val tmpFeatures = - TensorBuffer.createFixedSize(intArrayOf(IMAGE_SIZE, IMAGE_SIZE, 3), DataType.FLOAT32) - tmpFeatures.loadArray(floatBuffer) + val floatBuffer = resizedBitmap.toFloatArray(IMAGE_SIZE) + val tmpFeatures = TensorBuffer + .createFixedSize(intArrayOf(IMAGE_SIZE, IMAGE_SIZE, 3), DataType.FLOAT32) + .also { it.loadArray(floatBuffer) } + val imageTensorProcessor = TensorProcessor + .Builder() + .add(NormalizeOp(0.5f, 0.5f)) + .add(ReshapeOp()) + .build() val tensorBuffer = imageTensorProcessor.process(tmpFeatures).buffer - - val inputFeatures = - TensorBuffer.createFixedSize(intArrayOf(IMAGE_SIZE, IMAGE_SIZE, 3), DataType.FLOAT32) - inputFeatures.loadBuffer(tensorBuffer) - - val outputs = model.process(inputFeatures) - val outputFeature0 = outputs.outputFeature0AsTensorBuffer - - val floatArray = outputFeature0.floatArray ?: return ByteArray(0) - - return Utils.floatArrayToByteArray(floatArray) - } - - private fun Bitmap.toIntArray(imageSize: Int): IntArray { - val intValues = IntArray(imageSize * imageSize) - val resultArray = IntArray(imageSize * imageSize * 3) - - getPixels(intValues, 0, imageSize, 0, 0, imageSize, imageSize) - - var index = 0 - for (pixel in intValues) { - resultArray[index++] = (pixel shr 16) and 255 // Red - resultArray[index++] = (pixel shr 8) and 255 // Green - resultArray[index++] = pixel and 255 // Blue + val outputEmbeddingBuffer = Array(1) { FloatArray(OUTPUT_EMBEDDING_SIZE) } + + return try { + interpreter.run(tensorBuffer, outputEmbeddingBuffer) + Utils.floatArrayToByteArray(outputEmbeddingBuffer[0]) + } catch (e: Exception) { + println("Error running TFLite model inference, ${e.message}") + // Handle error, maybe return an empty array or throw a custom exception + ByteArray(0) } - return resultArray } }