react-native-biometrics-face 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +6 -0
- package/android/src/main/java/com/facerecognition/FaceRecognitionEngine.kt +182 -78
- package/android/src/main/java/com/facerecognition/FaceRecognitionModule.kt +2 -2
- package/android/src/main/java/com/facerecognition/FaceRecognitionPackage.kt +10 -2
- package/android/src/main/java/com/facerecognition/FaceRecognitionSpec.kt +2 -1
- package/android/src/main/java/com/facerecognition/LivenessAnalyzer.kt +75 -0
- package/android/src/main/java/com/facerecognition/LivenessCameraManager.kt +34 -0
- package/android/src/main/java/com/facerecognition/LivenessCameraView.kt +199 -0
- package/lib/module/index.js +29 -9
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +27 -7
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +63 -14
package/android/build.gradle
CHANGED
|
@@ -97,4 +97,10 @@ dependencies {
|
|
|
97
97
|
|
|
98
98
|
// 4. GPU Delegate (Optional but recommended for performance)
|
|
99
99
|
implementation 'org.tensorflow:tensorflow-lite-gpu:2.14.0'
|
|
100
|
+
|
|
101
|
+
def camerax_version = "1.3.0-alpha04" // or latest stable
|
|
102
|
+
implementation "androidx.camera:camera-core:${camerax_version}"
|
|
103
|
+
implementation "androidx.camera:camera-camera2:${camerax_version}"
|
|
104
|
+
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
|
|
105
|
+
implementation "androidx.camera:camera-view:${camerax_version}"
|
|
100
106
|
}
|
|
@@ -3,39 +3,46 @@ package com.facerecognition
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.graphics.Bitmap
|
|
5
5
|
import android.graphics.BitmapFactory
|
|
6
|
-
import android.graphics.
|
|
7
|
-
import android.
|
|
6
|
+
import android.graphics.Matrix
|
|
7
|
+
import android.graphics.RectF
|
|
8
|
+
import android.net.Uri
|
|
8
9
|
import com.google.mlkit.vision.common.InputImage
|
|
9
10
|
import com.google.mlkit.vision.face.FaceDetection
|
|
10
11
|
import com.google.mlkit.vision.face.FaceDetectorOptions
|
|
12
|
+
import com.google.mlkit.vision.face.FaceLandmark
|
|
13
|
+
import java.nio.ByteBuffer
|
|
14
|
+
import java.util.concurrent.CountDownLatch
|
|
15
|
+
import kotlin.math.abs
|
|
16
|
+
import kotlin.math.atan2
|
|
17
|
+
import kotlin.math.pow
|
|
18
|
+
import kotlin.math.sqrt
|
|
11
19
|
import org.tensorflow.lite.Interpreter
|
|
12
20
|
import org.tensorflow.lite.support.common.FileUtil
|
|
13
21
|
import org.tensorflow.lite.support.common.ops.NormalizeOp
|
|
14
22
|
import org.tensorflow.lite.support.image.ImageProcessor
|
|
15
23
|
import org.tensorflow.lite.support.image.TensorImage
|
|
16
24
|
import org.tensorflow.lite.support.image.ops.ResizeOp
|
|
17
|
-
import java.nio.ByteBuffer
|
|
18
|
-
import java.util.concurrent.CountDownLatch
|
|
19
|
-
import kotlin.math.pow
|
|
20
|
-
import kotlin.math.sqrt
|
|
21
25
|
|
|
22
|
-
class FaceRecognitionEngine(context: Context) {
|
|
26
|
+
class FaceRecognitionEngine(private val context: Context) {
|
|
23
27
|
|
|
24
28
|
// Configuration
|
|
25
29
|
private val MODEL_NAME = "mobile_face_net.tflite"
|
|
26
30
|
private val INPUT_SIZE = 112
|
|
27
|
-
private val OUTPUT_SIZE = 192
|
|
28
|
-
private val THRESHOLD = 1.0f
|
|
29
|
-
|
|
30
|
-
// ML Kit Detector
|
|
31
|
-
private val detectorOptions =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
private val OUTPUT_SIZE = 192
|
|
32
|
+
private val THRESHOLD = 1.0f
|
|
33
|
+
|
|
34
|
+
// ML Kit Detector - NOW WITH CLASSIFICATION (for Smile/Eyes)
|
|
35
|
+
private val detectorOptions =
|
|
36
|
+
FaceDetectorOptions.Builder()
|
|
37
|
+
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
|
|
38
|
+
.setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
|
|
39
|
+
.setClassificationMode(
|
|
40
|
+
FaceDetectorOptions.CLASSIFICATION_MODE_ALL
|
|
41
|
+
) // Enabled for Liveness
|
|
42
|
+
.build()
|
|
43
|
+
|
|
36
44
|
private val faceDetector = FaceDetection.getClient(detectorOptions)
|
|
37
45
|
|
|
38
|
-
// TFLite Interpreter
|
|
39
46
|
private var interpreter: Interpreter? = null
|
|
40
47
|
|
|
41
48
|
init {
|
|
@@ -47,110 +54,207 @@ class FaceRecognitionEngine(context: Context) {
|
|
|
47
54
|
}
|
|
48
55
|
}
|
|
49
56
|
|
|
50
|
-
fun verifyFaces(
|
|
51
|
-
if (interpreter == null)
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
fun verifyFaces(sourceUriStr: String, targetUriStr: String, livenessMode: String): ApiResponse {
|
|
58
|
+
if (interpreter == null) return ApiResponse(500, "Model failed to load", null)
|
|
59
|
+
|
|
60
|
+
val sourceBitmap = decodeUri(sourceUriStr)
|
|
61
|
+
val targetBitmap = decodeUri(targetUriStr)
|
|
54
62
|
|
|
55
|
-
// 1. Decode Images
|
|
56
|
-
val sourceBitmap = decodeBase64(base64Source)
|
|
57
|
-
val targetBitmap = decodeBase64(base64Target)
|
|
58
|
-
|
|
59
63
|
if (sourceBitmap == null || targetBitmap == null) {
|
|
60
|
-
return ApiResponse(400, "
|
|
64
|
+
return ApiResponse(400, "Failed to decode image from URI", null)
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
//
|
|
64
|
-
val sourceFace =
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
// 1. Process Source (Reference Image) - No liveness check needed
|
|
68
|
+
val sourceFace =
|
|
69
|
+
detectAlignAndCrop(sourceBitmap, checkLiveness = false, requiredMode = "NONE")
|
|
67
70
|
if (sourceFace.error != null) return sourceFace.error
|
|
71
|
+
|
|
72
|
+
// 2. Process Target (Live Selfie) - ENFORCE Liveness here
|
|
73
|
+
val targetFace =
|
|
74
|
+
detectAlignAndCrop(targetBitmap, checkLiveness = true, requiredMode = livenessMode)
|
|
68
75
|
if (targetFace.error != null) return targetFace.error
|
|
69
76
|
|
|
70
|
-
// 3. Generate Embeddings
|
|
77
|
+
// 3. Generate Embeddings & Compare
|
|
71
78
|
val sourceEmbedding = getEmbedding(sourceFace.bitmap!!)
|
|
72
79
|
val targetEmbedding = getEmbedding(targetFace.bitmap!!)
|
|
73
80
|
|
|
74
|
-
// 4. Calculate Distance (Euclidean)
|
|
75
81
|
var distance = 0f
|
|
76
82
|
for (i in sourceEmbedding.indices) {
|
|
77
83
|
distance += (sourceEmbedding[i] - targetEmbedding[i]).pow(2)
|
|
78
84
|
}
|
|
79
85
|
distance = sqrt(distance)
|
|
80
86
|
|
|
81
|
-
// 5. Calculate Accuracy (Simple mapping from distance)
|
|
82
|
-
// Note: This is a heuristic. distance 0 = 100%, distance > 1.2 = 0%
|
|
83
87
|
val accuracy = (1.0f - (distance / 2.0f)).coerceIn(0.0f, 1.0f) * 100
|
|
84
88
|
val isMatch = distance < THRESHOLD
|
|
85
89
|
|
|
86
|
-
return ApiResponse(200, "
|
|
90
|
+
return ApiResponse(200, "Success", VerificationResult(isMatch, distance, accuracy))
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
// --- Helper Classes & Methods ---
|
|
90
94
|
|
|
91
95
|
data class FaceResult(val bitmap: Bitmap? = null, val error: ApiResponse? = null)
|
|
92
96
|
|
|
93
|
-
private fun
|
|
97
|
+
private fun decodeUri(uriString: String): Bitmap? {
|
|
98
|
+
return try {
|
|
99
|
+
val uri = Uri.parse(uriString)
|
|
100
|
+
val inputStream = context.contentResolver.openInputStream(uri)
|
|
101
|
+
BitmapFactory.decodeStream(inputStream)
|
|
102
|
+
} catch (e: Exception) {
|
|
103
|
+
e.printStackTrace()
|
|
104
|
+
null
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Detects, Checks Liveness, Aligns, and Crops. */
|
|
109
|
+
private fun detectAlignAndCrop(
|
|
110
|
+
bitmap: Bitmap,
|
|
111
|
+
checkLiveness: Boolean,
|
|
112
|
+
requiredMode: String
|
|
113
|
+
): FaceResult {
|
|
94
114
|
val latch = CountDownLatch(1)
|
|
95
115
|
var result = FaceResult(error = ApiResponse(500, "Detection timeout"))
|
|
96
116
|
val inputImage = InputImage.fromBitmap(bitmap, 0)
|
|
97
117
|
|
|
98
|
-
faceDetector
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
faceDetector
|
|
119
|
+
.process(inputImage)
|
|
120
|
+
.addOnSuccessListener { faces ->
|
|
121
|
+
if (faces.isEmpty()) {
|
|
122
|
+
result = FaceResult(error = ApiResponse(400, "No face detected"))
|
|
123
|
+
} else {
|
|
124
|
+
val face = faces[0]
|
|
125
|
+
|
|
126
|
+
// --- LIVENESS CHECK START (Only for Target) ---
|
|
127
|
+
if (checkLiveness) {
|
|
128
|
+
// 1. Head Rotation Check (Must look at camera)
|
|
129
|
+
// Euler Y is the left/right head turn. We want it close to 0.
|
|
130
|
+
val rotY = face.headEulerAngleY
|
|
131
|
+
if (abs(rotY) > 12) { // Allow +/- 12 degrees
|
|
132
|
+
result =
|
|
133
|
+
FaceResult(
|
|
134
|
+
error =
|
|
135
|
+
ApiResponse(
|
|
136
|
+
400,
|
|
137
|
+
"Liveness Failed: Please look directly at the camera."
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
latch.countDown()
|
|
141
|
+
return@addOnSuccessListener
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 2. Smile Check
|
|
145
|
+
if (requiredMode == "SMILE") {
|
|
146
|
+
val smileProb = face.smilingProbability ?: 0f
|
|
147
|
+
if (smileProb < 0.8f) { // 80% confidence
|
|
148
|
+
result =
|
|
149
|
+
FaceResult(
|
|
150
|
+
error =
|
|
151
|
+
ApiResponse(
|
|
152
|
+
400,
|
|
153
|
+
"Liveness Failed: You must smile to verify."
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
latch.countDown()
|
|
157
|
+
return@addOnSuccessListener
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// --- LIVENESS CHECK END ---
|
|
162
|
+
|
|
163
|
+
// --- ALIGNMENT LOGIC ---
|
|
164
|
+
var finalBitmap = bitmap
|
|
165
|
+
var finalBounds = RectF(face.boundingBox)
|
|
166
|
+
val leftEye = face.getLandmark(FaceLandmark.LEFT_EYE)
|
|
167
|
+
val rightEye = face.getLandmark(FaceLandmark.RIGHT_EYE)
|
|
168
|
+
|
|
169
|
+
if (leftEye != null && rightEye != null) {
|
|
170
|
+
val deltaX = rightEye.position.x - leftEye.position.x
|
|
171
|
+
val deltaY = rightEye.position.y - leftEye.position.y
|
|
172
|
+
val angle = Math.toDegrees(atan2(deltaY.toDouble(), deltaX.toDouble()))
|
|
173
|
+
|
|
174
|
+
if (abs(angle) > 1.0) {
|
|
175
|
+
val matrix = Matrix()
|
|
176
|
+
val centerX = finalBounds.centerX()
|
|
177
|
+
val centerY = finalBounds.centerY()
|
|
178
|
+
matrix.postRotate(angle.toFloat(), centerX, centerY)
|
|
179
|
+
|
|
180
|
+
val rotatedBitmap =
|
|
181
|
+
Bitmap.createBitmap(
|
|
182
|
+
bitmap,
|
|
183
|
+
0,
|
|
184
|
+
0,
|
|
185
|
+
bitmap.width,
|
|
186
|
+
bitmap.height,
|
|
187
|
+
matrix,
|
|
188
|
+
true
|
|
189
|
+
)
|
|
190
|
+
matrix.mapRect(finalBounds)
|
|
191
|
+
finalBitmap = rotatedBitmap
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
val left = finalBounds.left.toInt().coerceAtLeast(0)
|
|
196
|
+
val top = finalBounds.top.toInt().coerceAtLeast(0)
|
|
197
|
+
val width =
|
|
198
|
+
finalBounds.width().toInt().coerceAtMost(finalBitmap.width - left)
|
|
199
|
+
val height =
|
|
200
|
+
finalBounds.height().toInt().coerceAtMost(finalBitmap.height - top)
|
|
201
|
+
|
|
202
|
+
if (width > 0 && height > 0) {
|
|
203
|
+
val cropped = Bitmap.createBitmap(finalBitmap, left, top, width, height)
|
|
204
|
+
result = FaceResult(bitmap = cropped)
|
|
205
|
+
} else {
|
|
206
|
+
result = FaceResult(error = ApiResponse(400, "Invalid face crop area"))
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
latch.countDown()
|
|
115
210
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
211
|
+
.addOnFailureListener {
|
|
212
|
+
result = FaceResult(error = ApiResponse(500, "Detection failed: ${it.message}"))
|
|
213
|
+
latch.countDown()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
latch.await()
|
|
218
|
+
} catch (e: InterruptedException) {}
|
|
124
219
|
return result
|
|
125
220
|
}
|
|
126
221
|
|
|
127
222
|
private fun getEmbedding(bitmap: Bitmap): FloatArray {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
223
|
+
val imageProcessor =
|
|
224
|
+
ImageProcessor.Builder()
|
|
225
|
+
.add(ResizeOp(INPUT_SIZE, INPUT_SIZE, ResizeOp.ResizeMethod.BILINEAR))
|
|
226
|
+
.add(NormalizeOp(127.5f, 127.5f)) // Normalize Input to [-1, 1]
|
|
227
|
+
.build()
|
|
133
228
|
|
|
134
229
|
var tensorImage = TensorImage.fromBitmap(bitmap)
|
|
135
230
|
tensorImage = imageProcessor.process(tensorImage)
|
|
136
231
|
|
|
137
|
-
val outputBuffer = ByteBuffer.allocateDirect(OUTPUT_SIZE * 4)
|
|
232
|
+
val outputBuffer = ByteBuffer.allocateDirect(OUTPUT_SIZE * 4)
|
|
138
233
|
outputBuffer.order(java.nio.ByteOrder.nativeOrder())
|
|
139
|
-
|
|
234
|
+
|
|
140
235
|
interpreter?.run(tensorImage.buffer, outputBuffer)
|
|
141
|
-
|
|
236
|
+
|
|
142
237
|
outputBuffer.rewind()
|
|
143
238
|
val floatArray = FloatArray(OUTPUT_SIZE)
|
|
144
239
|
outputBuffer.asFloatBuffer().get(floatArray)
|
|
145
|
-
return floatArray
|
|
146
|
-
}
|
|
147
240
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
241
|
+
// ===============================================================
|
|
242
|
+
// 🚨 CRITICAL FIX: L2 NORMALIZE THE EMBEDDING VECTOR 🚨
|
|
243
|
+
// ===============================================================
|
|
244
|
+
var sumSq = 0f
|
|
245
|
+
for (f in floatArray) {
|
|
246
|
+
sumSq += f * f
|
|
154
247
|
}
|
|
248
|
+
val norm = sqrt(sumSq)
|
|
249
|
+
|
|
250
|
+
// Divide every value by the vector's magnitude (norm)
|
|
251
|
+
if (norm > 0f) {
|
|
252
|
+
for (i in floatArray.indices) {
|
|
253
|
+
floatArray[i] = floatArray[i] / norm
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// ===============================================================
|
|
257
|
+
|
|
258
|
+
return floatArray
|
|
155
259
|
}
|
|
156
|
-
}
|
|
260
|
+
}
|
|
@@ -17,10 +17,10 @@ class FaceRecognitionModule(reactContext: ReactApplicationContext) :
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
@ReactMethod
|
|
20
|
-
override fun verifyFaces(
|
|
20
|
+
override fun verifyFaces(sourceUri: String, targetUri: String, livenessMode: String, promise: Promise) {
|
|
21
21
|
CoroutineScope(Dispatchers.IO).launch {
|
|
22
22
|
try {
|
|
23
|
-
val response = engine.verifyFaces(
|
|
23
|
+
val response = engine.verifyFaces(sourceUri, targetUri, livenessMode)
|
|
24
24
|
promise.resolve(response.toWritableMap())
|
|
25
25
|
} catch (e: Exception) {
|
|
26
26
|
val errorResponse = ApiResponse(500, "Native Module Error: ${e.message}")
|
|
@@ -5,9 +5,12 @@ import com.facebook.react.bridge.NativeModule
|
|
|
5
5
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
6
|
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
7
|
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
import com.facebook.react.uimanager.ViewManager // <--- Don't forget this import
|
|
8
9
|
import java.util.HashMap
|
|
9
10
|
|
|
10
11
|
class FaceRecognitionPackage : TurboReactPackage() {
|
|
12
|
+
|
|
13
|
+
// 1. Register Native Modules (Logic)
|
|
11
14
|
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
12
15
|
return if (name == FaceRecognitionModule.NAME) {
|
|
13
16
|
FaceRecognitionModule(reactContext)
|
|
@@ -16,12 +19,17 @@ class FaceRecognitionPackage : TurboReactPackage() {
|
|
|
16
19
|
}
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
// 2. Register Native Components (UI)
|
|
23
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
24
|
+
// Register the Camera Manager here so React Native can find <LivenessCameraView />
|
|
25
|
+
return listOf(LivenessCameraManager())
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 3. Register Package Info
|
|
19
29
|
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
20
30
|
return ReactModuleInfoProvider {
|
|
21
31
|
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
|
|
22
32
|
|
|
23
|
-
// FIX: Set this to false.
|
|
24
|
-
// This tells RN to load it via the standard Bridge, which matches our Java class.
|
|
25
33
|
val isTurboModule = false
|
|
26
34
|
|
|
27
35
|
moduleInfos[FaceRecognitionModule.NAME] = ReactModuleInfo(
|
|
@@ -7,5 +7,6 @@ import com.facebook.react.bridge.Promise
|
|
|
7
7
|
abstract class FaceRecognitionSpec(reactContext: ReactApplicationContext) :
|
|
8
8
|
ReactContextBaseJavaModule(reactContext) {
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// Direct String argument
|
|
11
|
+
abstract fun verifyFaces(sourceUri: String, targetUri: String, livenessMode: String, promise: Promise)
|
|
11
12
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
package com.facerecognition
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import androidx.camera.core.ImageAnalysis
|
|
5
|
+
import androidx.camera.core.ImageProxy
|
|
6
|
+
import com.google.mlkit.vision.common.InputImage
|
|
7
|
+
import com.google.mlkit.vision.face.FaceDetection
|
|
8
|
+
import com.google.mlkit.vision.face.FaceDetectorOptions
|
|
9
|
+
|
|
10
|
+
class LivenessAnalyzer(
|
|
11
|
+
private val livenessMode: String, // "BLINK" or "SMILE"
|
|
12
|
+
private val onLivenessDetected: () -> Unit
|
|
13
|
+
) : ImageAnalysis.Analyzer {
|
|
14
|
+
|
|
15
|
+
private val detector = FaceDetection.getClient(
|
|
16
|
+
FaceDetectorOptions.Builder()
|
|
17
|
+
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST) // Fast for video stream
|
|
18
|
+
.setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_NONE)
|
|
19
|
+
.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL) // Needed for eyes/smile
|
|
20
|
+
.build()
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
private var isDetected = false
|
|
24
|
+
private var lastAnalysisTime = 0L
|
|
25
|
+
|
|
26
|
+
@SuppressLint("UnsafeOptInUsageError")
|
|
27
|
+
override fun analyze(imageProxy: ImageProxy) {
|
|
28
|
+
// Throttle analysis to ~5 FPS to save battery
|
|
29
|
+
val currentTime = System.currentTimeMillis()
|
|
30
|
+
if (currentTime - lastAnalysisTime < 200) {
|
|
31
|
+
imageProxy.close()
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
lastAnalysisTime = currentTime
|
|
35
|
+
|
|
36
|
+
val mediaImage = imageProxy.image
|
|
37
|
+
if (mediaImage != null && !isDetected) {
|
|
38
|
+
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
|
|
39
|
+
|
|
40
|
+
detector.process(image)
|
|
41
|
+
.addOnSuccessListener { faces ->
|
|
42
|
+
if (faces.isNotEmpty()) {
|
|
43
|
+
val face = faces[0]
|
|
44
|
+
|
|
45
|
+
var passed = false
|
|
46
|
+
|
|
47
|
+
if (livenessMode == "BLINK") {
|
|
48
|
+
// Check if eyes are CLOSED
|
|
49
|
+
val leftOpen = face.leftEyeOpenProbability ?: 1f
|
|
50
|
+
val rightOpen = face.rightEyeOpenProbability ?: 1f
|
|
51
|
+
if (leftOpen < 0.4f && rightOpen < 0.4f) {
|
|
52
|
+
passed = true
|
|
53
|
+
}
|
|
54
|
+
} else if (livenessMode == "SMILE") {
|
|
55
|
+
// Check if Smiling
|
|
56
|
+
val smile = face.smilingProbability ?: 0f
|
|
57
|
+
if (smile > 0.8f) {
|
|
58
|
+
passed = true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (passed && !isDetected) {
|
|
63
|
+
isDetected = true // Prevent double triggering
|
|
64
|
+
onLivenessDetected()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
.addOnCompleteListener {
|
|
69
|
+
imageProxy.close()
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
imageProxy.close()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
package com.facerecognition
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.common.MapBuilder
|
|
4
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
5
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
6
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
7
|
+
|
|
8
|
+
class LivenessCameraManager : SimpleViewManager<LivenessCameraView>() {
|
|
9
|
+
|
|
10
|
+
override fun getName() = "LivenessCameraView"
|
|
11
|
+
|
|
12
|
+
override fun createViewInstance(reactContext: ThemedReactContext): LivenessCameraView {
|
|
13
|
+
return LivenessCameraView(reactContext)
|
|
14
|
+
// REMOVED: view.post { view.startCamera() }
|
|
15
|
+
// The View now handles this itself in onAttachedToWindow
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@ReactProp(name = "livenessMode")
|
|
19
|
+
fun setLivenessMode(view: LivenessCameraView, mode: String?) {
|
|
20
|
+
view.livenessMode = mode ?: "BLINK"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any>? {
|
|
24
|
+
return MapBuilder.of(
|
|
25
|
+
"onCapture",
|
|
26
|
+
MapBuilder.of("registrationName", "onCapture")
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override fun onDropViewInstance(view: LivenessCameraView) {
|
|
31
|
+
super.onDropViewInstance(view)
|
|
32
|
+
view.stopCamera()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
package com.facerecognition
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.pm.PackageManager
|
|
6
|
+
import android.graphics.Bitmap
|
|
7
|
+
import android.graphics.BitmapFactory
|
|
8
|
+
import android.graphics.Matrix
|
|
9
|
+
import android.net.Uri
|
|
10
|
+
import android.util.Log
|
|
11
|
+
import android.view.ViewGroup
|
|
12
|
+
import android.widget.FrameLayout
|
|
13
|
+
import android.widget.Toast
|
|
14
|
+
import androidx.camera.core.*
|
|
15
|
+
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
16
|
+
import androidx.camera.view.PreviewView
|
|
17
|
+
import androidx.core.content.ContextCompat
|
|
18
|
+
import androidx.lifecycle.LifecycleOwner
|
|
19
|
+
import com.facebook.react.bridge.Arguments
|
|
20
|
+
import com.facebook.react.bridge.ReactContext
|
|
21
|
+
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
22
|
+
import java.io.File
|
|
23
|
+
import java.io.FileOutputStream
|
|
24
|
+
import java.nio.ByteBuffer
|
|
25
|
+
import java.util.concurrent.ExecutorService
|
|
26
|
+
import java.util.concurrent.Executors
|
|
27
|
+
|
|
28
|
+
class LivenessCameraView(context: Context) : FrameLayout(context) {
|
|
29
|
+
|
|
30
|
+
private val previewView: PreviewView = PreviewView(context)
|
|
31
|
+
private var cameraProvider: ProcessCameraProvider? = null
|
|
32
|
+
private var imageCapture: ImageCapture? = null
|
|
33
|
+
private val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
|
34
|
+
|
|
35
|
+
var livenessMode: String = "BLINK"
|
|
36
|
+
private var isCameraStarted = false
|
|
37
|
+
|
|
38
|
+
init {
|
|
39
|
+
layoutParams = LayoutParams(
|
|
40
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
41
|
+
ViewGroup.LayoutParams.MATCH_PARENT
|
|
42
|
+
)
|
|
43
|
+
previewView.layoutParams = LayoutParams(
|
|
44
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
45
|
+
ViewGroup.LayoutParams.MATCH_PARENT
|
|
46
|
+
)
|
|
47
|
+
previewView.implementationMode = PreviewView.ImplementationMode.COMPATIBLE
|
|
48
|
+
previewView.scaleType = PreviewView.ScaleType.FILL_CENTER
|
|
49
|
+
addView(previewView)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override fun onAttachedToWindow() {
|
|
53
|
+
super.onAttachedToWindow()
|
|
54
|
+
if (!isCameraStarted) startCamera()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override fun onDetachedFromWindow() {
|
|
58
|
+
super.onDetachedFromWindow()
|
|
59
|
+
stopCamera()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override fun requestLayout() {
|
|
63
|
+
super.requestLayout()
|
|
64
|
+
post(measureAndLayout)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private val measureAndLayout = Runnable {
|
|
68
|
+
measure(
|
|
69
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
70
|
+
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
71
|
+
)
|
|
72
|
+
layout(left, top, right, bottom)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private fun startCamera() {
|
|
76
|
+
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
|
|
77
|
+
Toast.makeText(context, "ERR: Camera Permission Missing!", Toast.LENGTH_LONG).show()
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
|
82
|
+
cameraProviderFuture.addListener({
|
|
83
|
+
try {
|
|
84
|
+
cameraProvider = cameraProviderFuture.get()
|
|
85
|
+
bindCameraUseCases()
|
|
86
|
+
isCameraStarted = true
|
|
87
|
+
} catch (e: Exception) {
|
|
88
|
+
Log.e("LivenessCamera", "Failed to get camera provider", e)
|
|
89
|
+
}
|
|
90
|
+
}, ContextCompat.getMainExecutor(context))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private fun bindCameraUseCases() {
|
|
94
|
+
val cameraProvider = cameraProvider ?: return
|
|
95
|
+
|
|
96
|
+
val preview = Preview.Builder().build()
|
|
97
|
+
preview.setSurfaceProvider(previewView.surfaceProvider)
|
|
98
|
+
|
|
99
|
+
val imageAnalyzer = ImageAnalysis.Builder()
|
|
100
|
+
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
101
|
+
.build()
|
|
102
|
+
|
|
103
|
+
imageAnalyzer.setAnalyzer(cameraExecutor, LivenessAnalyzer(livenessMode) {
|
|
104
|
+
takePhoto()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
imageCapture = ImageCapture.Builder().build()
|
|
108
|
+
val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
cameraProvider.unbindAll()
|
|
112
|
+
val lifecycleOwner = getLifecycleOwner(context)
|
|
113
|
+
if (lifecycleOwner != null) {
|
|
114
|
+
cameraProvider.bindToLifecycle(
|
|
115
|
+
lifecycleOwner, cameraSelector, preview, imageCapture, imageAnalyzer
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
} catch (exc: Exception) {
|
|
119
|
+
Log.e("LivenessCamera", "Use case binding failed", exc)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private fun getLifecycleOwner(context: Context): LifecycleOwner? {
|
|
124
|
+
if (context is LifecycleOwner) return context
|
|
125
|
+
if (context is ReactContext) {
|
|
126
|
+
return context.currentActivity as? LifecycleOwner
|
|
127
|
+
}
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --- FIX: Manual Capture to Flip Image ---
|
|
132
|
+
private fun takePhoto() {
|
|
133
|
+
val imageCapture = imageCapture ?: return
|
|
134
|
+
|
|
135
|
+
// Use InMemory Capture to process the Bitmap before saving
|
|
136
|
+
imageCapture.takePicture(
|
|
137
|
+
ContextCompat.getMainExecutor(context),
|
|
138
|
+
object : ImageCapture.OnImageCapturedCallback() {
|
|
139
|
+
override fun onError(exc: ImageCaptureException) {
|
|
140
|
+
Log.e("LivenessCamera", "Capture Failed", exc)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
override fun onCaptureSuccess(image: ImageProxy) {
|
|
144
|
+
try {
|
|
145
|
+
// 1. Convert ImageProxy to Bitmap
|
|
146
|
+
val bitmap = imageProxyToBitmap(image)
|
|
147
|
+
|
|
148
|
+
// 2. Prepare Matrix for Rotation & Mirroring
|
|
149
|
+
val matrix = Matrix()
|
|
150
|
+
matrix.postRotate(image.imageInfo.rotationDegrees.toFloat())
|
|
151
|
+
|
|
152
|
+
// *** THE FIX: FLIP HORIZONTALLY ***
|
|
153
|
+
// This makes the saved file match the mirrored preview
|
|
154
|
+
matrix.postScale(-1f, 1f)
|
|
155
|
+
|
|
156
|
+
// 3. Create new Transformed Bitmap
|
|
157
|
+
val finalBitmap = Bitmap.createBitmap(
|
|
158
|
+
bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
// 4. Save to File
|
|
162
|
+
val photoFile = File(context.externalCacheDir, "liveness_${System.currentTimeMillis()}.jpg")
|
|
163
|
+
val out = FileOutputStream(photoFile)
|
|
164
|
+
finalBitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
|
|
165
|
+
out.flush()
|
|
166
|
+
out.close()
|
|
167
|
+
|
|
168
|
+
// 5. Send Result
|
|
169
|
+
val savedUri = Uri.fromFile(photoFile)
|
|
170
|
+
sendEvent(savedUri.toString())
|
|
171
|
+
|
|
172
|
+
} catch (e: Exception) {
|
|
173
|
+
Log.e("LivenessCamera", "Bitmap processing failed", e)
|
|
174
|
+
} finally {
|
|
175
|
+
image.close() // Always close the proxy
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private fun imageProxyToBitmap(image: ImageProxy): Bitmap {
|
|
183
|
+
val buffer: ByteBuffer = image.planes[0].buffer
|
|
184
|
+
val bytes = ByteArray(buffer.remaining())
|
|
185
|
+
buffer.get(bytes)
|
|
186
|
+
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private fun sendEvent(uri: String) {
|
|
190
|
+
val reactContext = context as ReactContext
|
|
191
|
+
val event = Arguments.createMap().apply { putString("uri", uri) }
|
|
192
|
+
reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "onCapture", event)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fun stopCamera() {
|
|
196
|
+
isCameraStarted = false
|
|
197
|
+
try { cameraProvider?.unbindAll() } catch(e: Exception) {}
|
|
198
|
+
}
|
|
199
|
+
}
|
package/lib/module/index.js
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { NativeModules, Platform, requireNativeComponent } from 'react-native';
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// --- 1. Define Types ---
|
|
6
|
+
|
|
7
|
+
// Type for the Liveness Camera Props
|
|
8
|
+
|
|
9
|
+
// --- 2. Safe Native Module Access ---
|
|
10
|
+
const LINKING_ERROR = `The package 'react-native-face-recognition' doesn't seem to be linked. Make sure: \n\n` + Platform.select({
|
|
11
|
+
ios: "- You have run 'pod install'\n",
|
|
12
|
+
default: ''
|
|
13
|
+
}) + '- You rebuilt the app after installing the package\n' + '- You are not using Expo Go\n';
|
|
14
|
+
const FaceRecognitionModule = NativeModules.FaceRecognition ? NativeModules.FaceRecognition : new Proxy({}, {
|
|
15
|
+
get() {
|
|
16
|
+
throw new Error(LINKING_ERROR);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// --- 3. Export Public API ---
|
|
6
21
|
|
|
7
22
|
/**
|
|
8
|
-
* Verifies if two faces match.
|
|
9
|
-
* * @param
|
|
10
|
-
* @param
|
|
11
|
-
* @
|
|
23
|
+
* Verifies if two faces match using the Native Engine.
|
|
24
|
+
* * @param sourceUri - URI of the reference photo (gallery)
|
|
25
|
+
* @param targetUri - URI of the live selfie (camera)
|
|
26
|
+
* @param livenessMode - (Optional) "SMILE" or "BLINK" or "NONE"
|
|
12
27
|
*/
|
|
13
|
-
export
|
|
14
|
-
return
|
|
15
|
-
}
|
|
28
|
+
export const verifyFaces = (sourceUri, targetUri, livenessMode = 'NONE') => {
|
|
29
|
+
return FaceRecognitionModule.verifyFaces(sourceUri, targetUri, livenessMode);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// --- 4. Export Native Camera Component ---
|
|
33
|
+
// This allows you to import { LivenessCameraView } from 'your-package'
|
|
34
|
+
export const LivenessCameraView = requireNativeComponent('LivenessCameraView');
|
|
35
|
+
export default FaceRecognitionModule;
|
|
16
36
|
//# sourceMappingURL=index.js.map
|
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["FaceRecognition","verifyFaces","
|
|
1
|
+
{"version":3,"names":["NativeModules","Platform","requireNativeComponent","LINKING_ERROR","select","ios","default","FaceRecognitionModule","FaceRecognition","Proxy","get","Error","verifyFaces","sourceUri","targetUri","livenessMode","LivenessCameraView"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SACEA,aAAa,EACbC,QAAQ,EACRC,sBAAsB,QACjB,cAAc;;AAGrB;;AAaA;;AAMA;AACA,MAAMC,aAAa,GACjB,wFAAwF,GACxFF,QAAQ,CAACG,MAAM,CAAC;EAAEC,GAAG,EAAE,gCAAgC;EAAEC,OAAO,EAAE;AAAG,CAAC,CAAC,GACvE,sDAAsD,GACtD,+BAA+B;AAEjC,MAAMC,qBAAqB,GAAGP,aAAa,CAACQ,eAAe,GACvDR,aAAa,CAACQ,eAAe,GAC7B,IAAIC,KAAK,CACP,CAAC,CAAC,EACF;EACEC,GAAGA,CAAA,EAAG;IACJ,MAAM,IAAIC,KAAK,CAACR,aAAa,CAAC;EAChC;AACF,CACF,CAAC;;AAEL;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMS,WAAW,GAAGA,CACzBC,SAAiB,EACjBC,SAAiB,EACjBC,YAAwC,GAAG,MAAM,KACf;EAClC,OAAOR,qBAAqB,CAACK,WAAW,CAACC,SAAS,EAAEC,SAAS,EAAEC,YAAY,CAAC;AAC9E,CAAC;;AAED;AACA;AACA,OAAO,MAAMC,kBAAkB,GAAGd,sBAAsB,CACtD,oBACF,CAAC;AAED,eAAeK,qBAAqB","ignoreList":[]}
|
|
@@ -1,10 +1,30 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export
|
|
1
|
+
import type { ViewProps } from 'react-native';
|
|
2
|
+
export interface FaceVerificationResult {
|
|
3
|
+
isMatch: boolean;
|
|
4
|
+
distance: number;
|
|
5
|
+
accuracy: number;
|
|
6
|
+
}
|
|
7
|
+
export interface VerificationResponse {
|
|
8
|
+
statusCode: number;
|
|
9
|
+
message: string;
|
|
10
|
+
result: FaceVerificationResult | null;
|
|
11
|
+
}
|
|
12
|
+
interface LivenessCameraProps extends ViewProps {
|
|
13
|
+
livenessMode?: 'BLINK' | 'SMILE';
|
|
14
|
+
onCapture?: (event: {
|
|
15
|
+
nativeEvent: {
|
|
16
|
+
uri: string;
|
|
17
|
+
};
|
|
18
|
+
}) => void;
|
|
19
|
+
}
|
|
20
|
+
declare const FaceRecognitionModule: any;
|
|
3
21
|
/**
|
|
4
|
-
* Verifies if two faces match.
|
|
5
|
-
* * @param
|
|
6
|
-
* @param
|
|
7
|
-
* @
|
|
22
|
+
* Verifies if two faces match using the Native Engine.
|
|
23
|
+
* * @param sourceUri - URI of the reference photo (gallery)
|
|
24
|
+
* @param targetUri - URI of the live selfie (camera)
|
|
25
|
+
* @param livenessMode - (Optional) "SMILE" or "BLINK" or "NONE"
|
|
8
26
|
*/
|
|
9
|
-
export declare
|
|
27
|
+
export declare const verifyFaces: (sourceUri: string, targetUri: string, livenessMode?: "SMILE" | "BLINK" | "NONE") => Promise<VerificationResponse>;
|
|
28
|
+
export declare const LivenessCameraView: import("react-native").HostComponent<LivenessCameraProps>;
|
|
29
|
+
export default FaceRecognitionModule;
|
|
10
30
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG9C,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,sBAAsB,GAAG,IAAI,CAAC;CACvC;AAGD,UAAU,mBAAoB,SAAQ,SAAS;IAC7C,YAAY,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IACjC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,KAAK,IAAI,CAAC;CAC/D;AASD,QAAA,MAAM,qBAAqB,KAStB,CAAC;AAIN;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GACtB,WAAW,MAAM,EACjB,WAAW,MAAM,EACjB,eAAc,OAAO,GAAG,OAAO,GAAG,MAAe,KAChD,OAAO,CAAC,oBAAoB,CAE9B,CAAC;AAIF,eAAO,MAAM,kBAAkB,2DAE9B,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
|
package/package.json
CHANGED
package/src/index.tsx
CHANGED
|
@@ -1,18 +1,67 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
NativeModules,
|
|
3
|
+
Platform,
|
|
4
|
+
requireNativeComponent
|
|
5
|
+
} from 'react-native';
|
|
6
|
+
import type { ViewProps } from 'react-native';
|
|
3
7
|
|
|
4
|
-
//
|
|
5
|
-
export
|
|
8
|
+
// --- 1. Define Types ---
|
|
9
|
+
export interface FaceVerificationResult {
|
|
10
|
+
isMatch: boolean;
|
|
11
|
+
distance: number;
|
|
12
|
+
accuracy: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface VerificationResponse {
|
|
16
|
+
statusCode: number;
|
|
17
|
+
message: string;
|
|
18
|
+
result: FaceVerificationResult | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Type for the Liveness Camera Props
|
|
22
|
+
interface LivenessCameraProps extends ViewProps {
|
|
23
|
+
livenessMode?: 'BLINK' | 'SMILE';
|
|
24
|
+
onCapture?: (event: { nativeEvent: { uri: string } }) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- 2. Safe Native Module Access ---
|
|
28
|
+
const LINKING_ERROR =
|
|
29
|
+
`The package 'react-native-face-recognition' doesn't seem to be linked. Make sure: \n\n` +
|
|
30
|
+
Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
|
|
31
|
+
'- You rebuilt the app after installing the package\n' +
|
|
32
|
+
'- You are not using Expo Go\n';
|
|
33
|
+
|
|
34
|
+
const FaceRecognitionModule = NativeModules.FaceRecognition
|
|
35
|
+
? NativeModules.FaceRecognition
|
|
36
|
+
: new Proxy(
|
|
37
|
+
{},
|
|
38
|
+
{
|
|
39
|
+
get() {
|
|
40
|
+
throw new Error(LINKING_ERROR);
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// --- 3. Export Public API ---
|
|
6
46
|
|
|
7
47
|
/**
|
|
8
|
-
* Verifies if two faces match.
|
|
9
|
-
* * @param
|
|
10
|
-
* @param
|
|
11
|
-
* @
|
|
48
|
+
* Verifies if two faces match using the Native Engine.
|
|
49
|
+
* * @param sourceUri - URI of the reference photo (gallery)
|
|
50
|
+
* @param targetUri - URI of the live selfie (camera)
|
|
51
|
+
* @param livenessMode - (Optional) "SMILE" or "BLINK" or "NONE"
|
|
12
52
|
*/
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
53
|
+
export const verifyFaces = (
|
|
54
|
+
sourceUri: string,
|
|
55
|
+
targetUri: string,
|
|
56
|
+
livenessMode: 'SMILE' | 'BLINK' | 'NONE' = 'NONE'
|
|
57
|
+
): Promise<VerificationResponse> => {
|
|
58
|
+
return FaceRecognitionModule.verifyFaces(sourceUri, targetUri, livenessMode);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// --- 4. Export Native Camera Component ---
|
|
62
|
+
// This allows you to import { LivenessCameraView } from 'your-package'
|
|
63
|
+
export const LivenessCameraView = requireNativeComponent<LivenessCameraProps>(
|
|
64
|
+
'LivenessCameraView'
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
export default FaceRecognitionModule;
|