Skip to content

Commit

Permalink
Update face liveness samples for kotlin and swift (#98)
Browse files Browse the repository at this point in the history
* update face liveness samples for kotlin and swift

* revert target sdk version

---------

Co-authored-by: Bhaven Dedhia <[email protected]>
  • Loading branch information
diljale and Bhaven Dedhia authored Jun 14, 2024
1 parent d69171c commit 73c46e8
Show file tree
Hide file tree
Showing 29 changed files with 331 additions and 235 deletions.
4 changes: 2 additions & 2 deletions samples/kotlin/face/FaceAnalyzerSample/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ plugins {
```
* You need to add the following dependencies to apps' build.grade `dependencies` section.
```
implementation "com.azure.ai:azure-ai-vision-common:0.17.0-beta.1"
implementation "com.azure.ai:azure-ai-vision-faceanalyzer:0.17.0-beta.1"
implementation "com.azure.ai:azure-ai-vision-common:0.17.1-beta.1"
implementation "com.azure.ai:azure-ai-vision-faceanalyzer:0.17.1-beta.1"
```
* You need to add repository in the settings.gradle for dependencyResolutionManagement
```
Expand Down
12 changes: 6 additions & 6 deletions samples/kotlin/face/FaceAnalyzerSample/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ android {
}

dependencies {
implementation group: 'net.sourceforge.streamsupport', name: 'android-retrofuture', version: '1.7.4'
implementation 'com.google.android.material:material:1.12.0'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "androidx.activity:activity-ktx:1.9.0"
implementation "com.azure.ai:azure-ai-vision-faceanalyzer:0.17.0-beta.1"
implementation "com.azure.android:azure-core-http-okhttp:1.0.0-beta.14"
}
implementation 'com.azure.android:azure-core-http-okhttp:1.0.0-beta.14'
implementation "androidx.activity:activity-ktx:1.7.2"
implementation "com.azure.ai:azure-ai-vision-common:0.17.1-beta.1"
implementation "com.azure.ai:azure-ai-vision-faceanalyzer:0.17.1-beta.1"
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@
</activity>
</application>

</manifest>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -29,32 +29,32 @@ import com.azure.android.core.credential.AccessToken
import com.azure.android.core.credential.TokenCredential
import com.azure.android.core.credential.TokenRequestContext
import org.threeten.bp.OffsetDateTime
import java.net.URL
import kotlin.math.sqrt
/***
* Sample class to fetch token for starting liveness session.
* It is recommended to fetch this token from app server for production as part of init section.
*/
class StringTokenCredential(token: String) : TokenCredential {
override fun getToken(
request: TokenRequestContext,
callback: TokenCredential.TokenCredentialCallback
) {
callback.onSuccess(_token)
}

private var _token: AccessToken? = null

init {
_token = AccessToken(token, OffsetDateTime.MAX)
}
}

/***
* Analyze activity performs one-time face analysis, using the default camera stream as input.
* Launches the result activity once the analyzed event is triggered.
*/
open class AnalyzeActivity : AppCompatActivity() {
/***
* Sample class to fetch token for starting liveness session.
* It is recommended to fetch this token from app server for production as part of init section.
*/
class StringTokenCredential(token: String) : TokenCredential {
override fun getToken(
request: TokenRequestContext,
callback: TokenCredential.TokenCredentialCallback
) {
callback.onSuccess(_token)
}

private var _token: AccessToken? = null

init {
_token = AccessToken(token, OffsetDateTime.MAX)
}
}

private lateinit var mSurfaceView: SurfaceView
private lateinit var mCameraPreviewLayout: FrameLayout
private lateinit var mBackgroundLayout: ConstraintLayout
Expand All @@ -81,12 +81,12 @@ open class AnalyzeActivity : AppCompatActivity() {
mCameraPreviewLayout.removeAllViews()
mCameraPreviewLayout.addView(mSurfaceView)
mCameraPreviewLayout.visibility = View.INVISIBLE
mInstructionsView = findViewById(R.id.instructionString);
mInstructionsView = findViewById(R.id.instructionString)
mBackgroundLayout = findViewById(R.id.activity_main_layout)
var analyzeModel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val analyzeModel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra("model", AnalyzeModel::class.java)
} else {
@Suppress("DEPRECATION") intent.getParcelableExtra<AnalyzeModel>("model")
@Suppress("DEPRECATION") intent.getParcelableExtra("model")
}
mFaceApiEndpoint = analyzeModel?.endpoint
mSessionToken = analyzeModel?.token
Expand All @@ -99,14 +99,16 @@ open class AnalyzeActivity : AppCompatActivity() {

override fun onResume() {
super.onResume()
initializeConfig()
val visionSourceOptions = VisionSourceOptions(this, this as LifecycleOwner)
visionSourceOptions.setPreview(mSurfaceView)
mVisionSource = VisionSource.fromDefaultCamera(visionSourceOptions)
displayCameraOnLayout()

// Initialize faceAnalyzer with default camera as vision source
createFaceAnalyzer()
if (mFaceAnalyzer == null) {
initializeConfig()
val visionSourceOptions = VisionSourceOptions(this, this as LifecycleOwner)
visionSourceOptions.setPreview(mSurfaceView)
mVisionSource = VisionSource.fromDefaultCamera(visionSourceOptions)
displayCameraOnLayout()

// Initialize faceAnalyzer with default camera as vision source
createFaceAnalyzer()
}
startAnalyzeOnce()
}

Expand Down Expand Up @@ -156,39 +158,46 @@ open class AnalyzeActivity : AppCompatActivity() {

mFaceAnalyzer?.apply {
this.analyzed.addEventListener(analyzedListener)
this.analyzing.addEventListener(analyzinglistener)
this.analyzing.addEventListener(analyzingListener)
this.stopped.addEventListener(stoppedListener)
}
}

/**
* Listener for Analyzing callback. Receives tracking and user feedback information
*/
protected var analyzinglistener =
@Suppress("MemberVisibilityCanBePrivate")
protected var analyzingListener =
EventListener<FaceAnalyzingEventArgs> { _, e ->

e.result.use { result ->
if (result.faces.isNotEmpty()) {
// Get the first face in result
var face = result.faces.iterator().next()
val face = result.faces.iterator().next()

// Lighten/darken the screen based on liveness feedback
var requiredAction = face.actionRequiredFromApplicationTask?.action;
if (requiredAction == ActionRequiredFromApplication.BRIGHTEN_DISPLAY) {
mBackgroundLayout.setBackgroundColor(Color.WHITE)
face.actionRequiredFromApplicationTask.setAsCompleted()
} else if (requiredAction == ActionRequiredFromApplication.DARKEN_DISPLAY) {
mBackgroundLayout.setBackgroundColor(Color.BLACK)
face.actionRequiredFromApplicationTask.setAsCompleted()
} else if (requiredAction == ActionRequiredFromApplication.STOP_CAMERA) {
mCameraPreviewLayout.visibility = View.INVISIBLE
face.actionRequiredFromApplicationTask.setAsCompleted()
val requiredAction = face.actionRequiredFromApplicationTask?.action
when (requiredAction) {
ActionRequiredFromApplication.BRIGHTEN_DISPLAY -> {
mBackgroundLayout.setBackgroundColor(Color.WHITE)
face.actionRequiredFromApplicationTask.setAsCompleted()
}
ActionRequiredFromApplication.DARKEN_DISPLAY -> {
mBackgroundLayout.setBackgroundColor(Color.BLACK)
face.actionRequiredFromApplicationTask.setAsCompleted()
}
ActionRequiredFromApplication.STOP_CAMERA -> {
mCameraPreviewLayout.visibility = View.INVISIBLE
face.actionRequiredFromApplicationTask.setAsCompleted()
}
else -> {}
}

// Display user feedback and warnings on UI
if (!mDoneAnalyzing) {
var feedbackMessage = MapFeedbackToMessage(FeedbackForFace.NONE)
var feedbackMessage = mapFeedbackToMessage(FeedbackForFace.NONE)
if (face.feedbackForFace != null) {
feedbackMessage = MapFeedbackToMessage(face.feedbackForFace)
feedbackMessage = mapFeedbackToMessage(face.feedbackForFace)
}

val currentTime = System.currentTimeMillis()
Expand All @@ -210,18 +219,19 @@ open class AnalyzeActivity : AppCompatActivity() {
* Receives recognition and liveness result.
* Launches result activity.
*/
@Suppress("MemberVisibilityCanBePrivate")
protected var analyzedListener =
EventListener<FaceAnalyzedEventArgs> { _, e ->
val bd = Bundle()
e.result.use { result ->
if (result.faces.isNotEmpty()) {
// Get the first face in result
var face = result.faces.iterator().next()
var livenessStatus: LivenessStatus = face.livenessResult?.livenessStatus?: LivenessStatus.FAILED
var livenessFailureReason = face.livenessResult?.livenessFailureReason?: LivenessFailureReason.NONE
var verifyStatus = face.recognitionResult?.recognitionStatus?:RecognitionStatus.NOT_COMPUTED
var verifyConfidence = face.recognitionResult?.confidence?:Float.NaN
var resultIdsList: ArrayList<String> = ArrayList()
val face = result.faces.iterator().next()
val livenessStatus: LivenessStatus = face.livenessResult?.livenessStatus?: LivenessStatus.FAILED
val livenessFailureReason = face.livenessResult?.livenessFailureReason?: LivenessFailureReason.NONE
val verifyStatus = face.recognitionResult?.recognitionStatus?:RecognitionStatus.NOT_COMPUTED
val verifyConfidence = face.recognitionResult?.confidence?:Float.NaN
val resultIdsList: ArrayList<String> = ArrayList()
if (face.livenessResult.resultId != null) {
resultIdsList.add(face.livenessResult.resultId.toString())
}
Expand All @@ -242,6 +252,14 @@ open class AnalyzeActivity : AppCompatActivity() {
}
}

@Suppress("MemberVisibilityCanBePrivate")
protected var stoppedListener =
EventListener<FaceAnalysisStoppedEventArgs> { _, e ->
if (e.reason == FaceAnalysisStoppedReason.ERROR) {
mResultReceiver?.send(AnalyzedResultType.ERROR, null)
}
}

/**
* Sets faceAnalysisOptions and recognitionMode. Calls analyzeOnce.
*/
Expand All @@ -260,12 +278,12 @@ open class AnalyzeActivity : AppCompatActivity() {
return
}

mFaceAnalysisOptions = FaceAnalysisOptions();
mFaceAnalysisOptions = FaceAnalysisOptions()

mFaceAnalysisOptions?.setFaceSelectionMode(FaceSelectionMode.LARGEST)

try {
mFaceAnalyzer?.analyzeOnceAsync(mFaceAnalysisOptions);
mFaceAnalyzer?.analyzeOnceAsync(mFaceAnalysisOptions)
} catch (ex: Exception) {
ex.printStackTrace()
}
Expand All @@ -282,7 +300,7 @@ open class AnalyzeActivity : AppCompatActivity() {
* Displays camera stream on UI in a circular shape.
*/
private fun displayCameraOnLayout() {
val previewSize = mVisionSource?.getCameraPreviewFormat()
val previewSize = mVisionSource?.cameraPreviewFormat
val params = mCameraPreviewLayout.layoutParams as ConstraintLayout.LayoutParams
params.dimensionRatio = previewSize?.height.toString() + ":" + previewSize?.width
params.width = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT
Expand All @@ -295,17 +313,18 @@ open class AnalyzeActivity : AppCompatActivity() {
/**
* Override back button to always return to main activity
*/
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
synchronized(this) {
mBackPressed = true
}
@Suppress("DEPRECATION") super.onBackPressed()
mFaceAnalyzer?.stopAnalyzeOnce();
mFaceAnalyzer?.stopAnalyzeOnce()
val bd = Bundle()
mResultReceiver?.send(AnalyzedResultType.BACKPRESSED, bd)
}

private fun MapFeedbackToMessage(feedback : FeedbackForFace): String {
private fun mapFeedbackToMessage(feedback : FeedbackForFace): String {
when(feedback) {
FeedbackForFace.NONE -> return getString(R.string.feedback_none)
FeedbackForFace.LOOK_AT_CAMERA -> return getString(R.string.feedback_look_at_camera)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import android.content.Intent
import android.net.Uri
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import com.example.FaceAnalyzerSample.app.Utils
import com.example.FaceAnalyzerSample.Utils
import java.io.IOException
import java.io.ByteArrayOutputStream

Expand All @@ -27,7 +27,7 @@ object AppUtils {
val faceApiKey = sharedPref.getString("key", "").toString()
val sendResultsToClient = sharedPref.getBoolean("sendResultsToClient", false)

Utils.getFaceAPISessionToken(faceApiEndpoint, faceApiKey, verifyImage, sendResultsToClient)
Utils.getFaceAPISessionToken(faceApiEndpoint, faceApiKey, verifyImage, sendResultsToClient, context.contentResolver)
}
// Function to retrieve a string value from SharedPreferences
fun getVerifyImage(context: Context, uri: Uri) : ByteArray {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,4 @@ class AutoFitSurfaceView @JvmOverloads constructor(
clipToOutline = true
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.example.FaceAnalyzerSample.app.Utils
import com.example.FaceAnalyzerSample.Utils
import java.io.InputStream

/***
Expand Down Expand Up @@ -62,26 +62,9 @@ open class MainActivity : AppCompatActivity() {
mPickMedia = registerForActivityResult(PickImage()) { uri ->
if (uri != null) {
mVerifyImage = AppUtils.getVerifyImage(this, uri)
val orientation = this.applicationContext.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
arrayOf(MediaStore.Images.ImageColumns.ORIENTATION),
MediaStore.Images.Media._ID + " = ?",
arrayOf(DocumentsContract.getDocumentId(uri).split(":")[1]),
null).use {
return@use if (it == null || it.count != 1 || !it.moveToFirst()) null else
when (it.getInt(0)) {
-270 -> ExifInterface.ORIENTATION_ROTATE_90
-180 -> ExifInterface.ORIENTATION_ROTATE_180
-90 -> ExifInterface.ORIENTATION_ROTATE_270
90 -> ExifInterface.ORIENTATION_ROTATE_90
180 -> ExifInterface.ORIENTATION_ROTATE_180
270 -> ExifInterface.ORIENTATION_ROTATE_270
else -> null
}
}
this.applicationContext.contentResolver.openInputStream(uri).use { inputStream ->
if (inputStream != null) {
showImage(inputStream, orientation)
showImage(inputStream)
}
}
}
Expand All @@ -95,13 +78,14 @@ open class MainActivity : AppCompatActivity() {
}
}
@SuppressLint("NewApi")
private fun showImage(inputStream: InputStream, knownOrientationExifEnum: Int?) {
private fun showImage(inputStream: InputStream) {
var bitmapImage =
BitmapFactory.decodeStream(inputStream)

try { // rotate bitmap (best effort)
val matrix = Matrix()
(knownOrientationExifEnum ?: ExifInterface(inputStream).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL))
ExifInterface(inputStream)
.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
.let { orientation ->
when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90F)
Expand Down
Loading

0 comments on commit 73c46e8

Please sign in to comment.