react-native-vision-camera-spoof-detector 1.1.11 → 1.1.16
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/VisionCameraSpoofDetector.podspec +41 -0
- package/android/src/main/java/com/faceantispoof/FaceAntiSpoofingAdvanced.kt +172 -0
- package/android/src/main/java/com/margelo/nitro/camera/spoofdetector/HybridSpoofDetectorOutput.kt +48 -11
- package/ios/FaceAntiSpoofManager.swift +106 -4
- package/ios/FaceAntiSpoofModule.m +38 -51
- package/ios/HybridSpoofDetectorFactory.swift +10 -0
- package/ios/HybridSpoofDetectorOutput.swift +164 -0
- package/nitro.json +4 -0
- package/nitrogen/generated/ios/VisionCameraSpoofDetector+autolinking.rb +62 -0
- package/nitrogen/generated/ios/VisionCameraSpoofDetector-Swift-Cxx-Bridge.cpp +60 -0
- package/nitrogen/generated/ios/VisionCameraSpoofDetector-Swift-Cxx-Bridge.hpp +151 -0
- package/nitrogen/generated/ios/VisionCameraSpoofDetector-Swift-Cxx-Umbrella.hpp +60 -0
- package/nitrogen/generated/ios/VisionCameraSpoofDetectorAutolinking.mm +33 -0
- package/nitrogen/generated/ios/VisionCameraSpoofDetectorAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridSpoofDetectorFactorySpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridSpoofDetectorFactorySpecSwift.hpp +97 -0
- package/nitrogen/generated/ios/swift/AntiSpoofingResult.swift +54 -0
- package/nitrogen/generated/ios/swift/Func_void_AntiSpoofingResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridSpoofDetectorFactorySpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridSpoofDetectorFactorySpec_cxx.swift +142 -0
- package/nitrogen/generated/ios/swift/Size.swift +34 -0
- package/nitrogen/generated/ios/swift/SpoofDetectorOutputOptions.swift +74 -0
- package/package.json +8 -3
- package/src/specs/SpoofDetectorFactory.nitro.ts +1 -0
- package/ios/FaceAntiSpoofFrameProcessor.swift +0 -301
- package/ios/FaceAntiSpoofJSI.h +0 -12
- package/ios/FaceAntiSpoofJSI.mm +0 -92
- package/ios/FaceAntiSpoofPluginRegister.m +0 -84
- package/ios/models/FaceAntiSpoofing.tflite +0 -0
- package/react-native-vision-camera-spoof-detector.podspec +0 -22
- /package/{ios/models → lib/assets}/model_spec.json +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'VisionCameraSpoofDetector'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.homepage = package['homepage']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.authors = package['author']
|
|
12
|
+
s.platform = :ios, '15.5'
|
|
13
|
+
s.source = { :path => '.' }
|
|
14
|
+
s.swift_version = '5.0'
|
|
15
|
+
s.pod_target_xcconfig = {
|
|
16
|
+
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64',
|
|
17
|
+
'HEADER_SEARCH_PATHS' => '$(inherited) ${PODS_TARGET_SRCROOT}/nitrogen/generated/shared/c++ ${PODS_TARGET_SRCROOT}/nitrogen/generated/ios ${PODS_TARGET_SRCROOT}/nitrogen/generated/ios/c++'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
load 'nitrogen/generated/ios/VisionCameraSpoofDetector+autolinking.rb'
|
|
21
|
+
add_nitrogen_files(s)
|
|
22
|
+
|
|
23
|
+
s.source_files = [
|
|
24
|
+
'ios/**/*.{h,m,mm,swift,cpp,c,cc}',
|
|
25
|
+
'nitrogen/generated/shared/**/*.cpp',
|
|
26
|
+
'nitrogen/generated/ios/**/*.{cpp,mm,swift}',
|
|
27
|
+
'nitrogen/generated/ios/VisionCameraSpoofDetector-Swift-Cxx-Bridge.hpp',
|
|
28
|
+
]
|
|
29
|
+
s.public_header_files = 'nitrogen/generated/ios/VisionCameraSpoofDetector-Swift-Cxx-Bridge.hpp'
|
|
30
|
+
s.private_header_files = [
|
|
31
|
+
'nitrogen/generated/shared/**/*.hpp',
|
|
32
|
+
'nitrogen/generated/ios/c++/**/*.{h,hpp}',
|
|
33
|
+
]
|
|
34
|
+
s.resources = ['lib/assets/*']
|
|
35
|
+
|
|
36
|
+
s.dependency 'TensorFlowLiteSwift'
|
|
37
|
+
s.dependency 'VisionCamera'
|
|
38
|
+
s.dependency 'React-jsi'
|
|
39
|
+
s.dependency 'React-callinvoker'
|
|
40
|
+
install_modules_dependencies(s)
|
|
41
|
+
end
|
|
@@ -184,6 +184,49 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
fun advancedAntiSpoofingRGBA(rgba: ByteArray, width: Int, height: Int): AntiSpoofingResult {
|
|
188
|
+
if (!isInitialized) {
|
|
189
|
+
return AntiSpoofingResult(
|
|
190
|
+
neuralNetworkScore = 0f,
|
|
191
|
+
laplacianScore = 0,
|
|
192
|
+
textureComplexity = 0,
|
|
193
|
+
temporalConsistency = 0f,
|
|
194
|
+
colorSaturation = 0f,
|
|
195
|
+
combinedScore = 0f,
|
|
196
|
+
confidence = 0f
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return try {
|
|
201
|
+
val nnScore = neuralNetworkAntiSpoofingRGBA(rgba, width, height).getOrThrow()
|
|
202
|
+
val laplacianScore = laplacianFromRgba(rgba, width, height).getOrThrow()
|
|
203
|
+
val isLive = nnScore < BASE_THRESHOLD && laplacianScore > LAPLACIAN_THRESHOLD
|
|
204
|
+
val combinedScore = calculateCombinedScore(nnScore, laplacianScore)
|
|
205
|
+
val confidence = calculateConfidence(nnScore, laplacianScore)
|
|
206
|
+
|
|
207
|
+
AntiSpoofingResult(
|
|
208
|
+
neuralNetworkScore = nnScore,
|
|
209
|
+
laplacianScore = laplacianScore,
|
|
210
|
+
textureComplexity = 0,
|
|
211
|
+
temporalConsistency = 0f,
|
|
212
|
+
colorSaturation = 0f,
|
|
213
|
+
combinedScore = combinedScore,
|
|
214
|
+
confidence = confidence
|
|
215
|
+
)
|
|
216
|
+
} catch (e: Exception) {
|
|
217
|
+
Log.e(TAG, "Error in advancedAntiSpoofingRGBA", e)
|
|
218
|
+
AntiSpoofingResult(
|
|
219
|
+
neuralNetworkScore = 0f,
|
|
220
|
+
laplacianScore = 0,
|
|
221
|
+
textureComplexity = 0,
|
|
222
|
+
temporalConsistency = 0f,
|
|
223
|
+
colorSaturation = 0f,
|
|
224
|
+
combinedScore = 0f,
|
|
225
|
+
confidence = 0f
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
187
230
|
// FROM WORKING VERSION: neuralNetworkAntiSpoofing with Bitmap
|
|
188
231
|
private fun neuralNetworkAntiSpoofing(bitmap: Bitmap): Result<Float> {
|
|
189
232
|
return try {
|
|
@@ -216,6 +259,29 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
216
259
|
}
|
|
217
260
|
}
|
|
218
261
|
|
|
262
|
+
private fun neuralNetworkAntiSpoofingRGBA(rgba: ByteArray, width: Int, height: Int): Result<Float> {
|
|
263
|
+
return try {
|
|
264
|
+
val img = normalizeRgbaImage(rgba, width, height, INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE)
|
|
265
|
+
val input = arrayOf(img)
|
|
266
|
+
|
|
267
|
+
clssPred[0].fill(0f)
|
|
268
|
+
leafNodeMask[0].fill(0f)
|
|
269
|
+
|
|
270
|
+
outputs[outputIndex0] = clssPred
|
|
271
|
+
outputs[outputIndex1] = leafNodeMask
|
|
272
|
+
|
|
273
|
+
interpreter!!.runForMultipleInputsOutputs(arrayOf(input), outputs)
|
|
274
|
+
|
|
275
|
+
Log.i(TAG, "clss_pred: [${clssPred[0].joinToString()}]")
|
|
276
|
+
Log.i(TAG, "leaf_node_mask: [${leafNodeMask[0].joinToString()}]")
|
|
277
|
+
|
|
278
|
+
Result.success(leafScore1(clssPred, leafNodeMask))
|
|
279
|
+
} catch (e: Exception) {
|
|
280
|
+
Log.e(TAG, "Error in neuralNetworkAntiSpoofingRGBA", e)
|
|
281
|
+
Result.failure(e)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
219
285
|
// FROM WORKING VERSION: leafScore1
|
|
220
286
|
private fun leafScore1(clssPred: Array<FloatArray>, leafNodeMask: Array<FloatArray>): Float {
|
|
221
287
|
var score = 0f
|
|
@@ -247,6 +313,33 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
247
313
|
return floatValues
|
|
248
314
|
}
|
|
249
315
|
|
|
316
|
+
private fun normalizeRgbaImage(
|
|
317
|
+
rgba: ByteArray,
|
|
318
|
+
srcWidth: Int,
|
|
319
|
+
srcHeight: Int,
|
|
320
|
+
targetWidth: Int,
|
|
321
|
+
targetHeight: Int
|
|
322
|
+
): Array<Array<FloatArray>> {
|
|
323
|
+
val floatValues = Array(targetHeight) { Array(targetWidth) { FloatArray(3) } }
|
|
324
|
+
val scaleX = srcWidth.toFloat() / targetWidth
|
|
325
|
+
val scaleY = srcHeight.toFloat() / targetHeight
|
|
326
|
+
|
|
327
|
+
for (targetY in 0 until targetHeight) {
|
|
328
|
+
val srcY = (targetY * scaleY).toInt().coerceIn(0, srcHeight - 1)
|
|
329
|
+
for (targetX in 0 until targetWidth) {
|
|
330
|
+
val srcX = (targetX * scaleX).toInt().coerceIn(0, srcWidth - 1)
|
|
331
|
+
val srcIndex = (srcY * srcWidth + srcX) * 4
|
|
332
|
+
|
|
333
|
+
val r = (rgba[srcIndex].toInt() and 0xFF) / 255f
|
|
334
|
+
val g = (rgba[srcIndex + 1].toInt() and 0xFF) / 255f
|
|
335
|
+
val b = (rgba[srcIndex + 2].toInt() and 0xFF) / 255f
|
|
336
|
+
floatValues[targetY][targetX] = floatArrayOf(r, g, b)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return floatValues
|
|
341
|
+
}
|
|
342
|
+
|
|
250
343
|
// FROM WORKING VERSION: laplacian with Bitmap
|
|
251
344
|
private fun laplacian(bitmap: Bitmap): Result<Int> {
|
|
252
345
|
return try {
|
|
@@ -264,6 +357,17 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
264
357
|
}
|
|
265
358
|
}
|
|
266
359
|
|
|
360
|
+
private fun laplacianFromRgba(rgba: ByteArray, width: Int, height: Int): Result<Int> {
|
|
361
|
+
return try {
|
|
362
|
+
val gray = rgbaToGrayscale(rgba, width, height, INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE)
|
|
363
|
+
val score = calculateLaplacianFromGray(gray)
|
|
364
|
+
Result.success(score)
|
|
365
|
+
} catch (e: Exception) {
|
|
366
|
+
Log.e(TAG, "Error in laplacianFromRgba", e)
|
|
367
|
+
Result.failure(e)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
267
371
|
// FROM WORKING VERSION: calculateLaplacianScore
|
|
268
372
|
private fun calculateLaplacianScore(bitmap: Bitmap): Int {
|
|
269
373
|
val laplace = arrayOf(
|
|
@@ -315,6 +419,58 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
315
419
|
return greyImg
|
|
316
420
|
}
|
|
317
421
|
|
|
422
|
+
private fun rgbaToGrayscale(
|
|
423
|
+
rgba: ByteArray,
|
|
424
|
+
srcWidth: Int,
|
|
425
|
+
srcHeight: Int,
|
|
426
|
+
targetWidth: Int,
|
|
427
|
+
targetHeight: Int
|
|
428
|
+
): Array<IntArray> {
|
|
429
|
+
val gray = Array(targetHeight) { IntArray(targetWidth) }
|
|
430
|
+
val scaleX = srcWidth.toFloat() / targetWidth
|
|
431
|
+
val scaleY = srcHeight.toFloat() / targetHeight
|
|
432
|
+
|
|
433
|
+
for (targetY in 0 until targetHeight) {
|
|
434
|
+
val srcY = (targetY * scaleY).toInt().coerceIn(0, srcHeight - 1)
|
|
435
|
+
for (targetX in 0 until targetWidth) {
|
|
436
|
+
val srcX = (targetX * scaleX).toInt().coerceIn(0, srcWidth - 1)
|
|
437
|
+
val srcIndex = (srcY * srcWidth + srcX) * 4
|
|
438
|
+
val r = rgba[srcIndex].toInt() and 0xFF
|
|
439
|
+
val g = rgba[srcIndex + 1].toInt() and 0xFF
|
|
440
|
+
val b = rgba[srcIndex + 2].toInt() and 0xFF
|
|
441
|
+
gray[targetY][targetX] = (0.299 * r + 0.587 * g + 0.114 * b).toInt()
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return gray
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private fun calculateLaplacianFromGray(img: Array<IntArray>): Int {
|
|
449
|
+
val laplace = arrayOf(
|
|
450
|
+
intArrayOf(0, 1, 0),
|
|
451
|
+
intArrayOf(1, -4, 1),
|
|
452
|
+
intArrayOf(0, 1, 0)
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
val height = img.size
|
|
456
|
+
val width = img[0].size
|
|
457
|
+
var score = 0
|
|
458
|
+
for (x in 0 until height - laplace.size + 1) {
|
|
459
|
+
for (y in 0 until width - laplace.size + 1) {
|
|
460
|
+
var result = 0
|
|
461
|
+
for (i in 0 until laplace.size) {
|
|
462
|
+
for (j in 0 until laplace.size) {
|
|
463
|
+
result += img[x + i][y + j] * laplace[i][j]
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (result > LAPLACE_THRESHOLD) {
|
|
467
|
+
score++
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return score
|
|
472
|
+
}
|
|
473
|
+
|
|
318
474
|
// FROM WORKING VERSION: calculateCombinedScore
|
|
319
475
|
private fun calculateCombinedScore(neuralScore: Float, laplacianScore: Int): Float {
|
|
320
476
|
// Normalize both scores to 0-1 range
|
|
@@ -422,6 +578,22 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
422
578
|
return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888)
|
|
423
579
|
}
|
|
424
580
|
|
|
581
|
+
private fun rgbaToBitmap(rgba: ByteArray, width: Int, height: Int): Bitmap {
|
|
582
|
+
val pixels = IntArray(width * height)
|
|
583
|
+
var index = 0
|
|
584
|
+
for (y in 0 until height) {
|
|
585
|
+
for (x in 0 until width) {
|
|
586
|
+
val r = rgba[index].toInt() and 0xFF
|
|
587
|
+
val g = rgba[index + 1].toInt() and 0xFF
|
|
588
|
+
val b = rgba[index + 2].toInt() and 0xFF
|
|
589
|
+
val a = rgba[index + 3].toInt() and 0xFF
|
|
590
|
+
pixels[y * width + x] = (a shl 24) or (r shl 16) or (g shl 8) or b
|
|
591
|
+
index += 4
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888)
|
|
595
|
+
}
|
|
596
|
+
|
|
425
597
|
@Throws(IOException::class)
|
|
426
598
|
private fun loadModelFile(assetManager: AssetManager, modelName: String): MappedByteBuffer {
|
|
427
599
|
return try {
|
package/android/src/main/java/com/margelo/nitro/camera/spoofdetector/HybridSpoofDetectorOutput.kt
CHANGED
|
@@ -53,7 +53,7 @@ class HybridSpoofDetectorOutput(
|
|
|
53
53
|
.build()
|
|
54
54
|
}
|
|
55
55
|
val analysis = ImageAnalysis.Builder()
|
|
56
|
-
.setOutputImageFormat(ImageAnalysis.
|
|
56
|
+
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
|
|
57
57
|
.setOutputImageRotationEnabled(false)
|
|
58
58
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
59
59
|
.setAllowDroppingLateFrames(true)
|
|
@@ -71,16 +71,8 @@ class HybridSpoofDetectorOutput(
|
|
|
71
71
|
return
|
|
72
72
|
}
|
|
73
73
|
try {
|
|
74
|
-
val
|
|
75
|
-
val
|
|
76
|
-
val buffer = sourcePlanes[index].buffer.duplicate()
|
|
77
|
-
ByteArray(buffer.remaining()).also(buffer::get)
|
|
78
|
-
}
|
|
79
|
-
val rowStrides = IntArray(3) { sourcePlanes[it].rowStride }
|
|
80
|
-
val pixelStrides = IntArray(3) { sourcePlanes[it].pixelStride.coerceAtLeast(1) }
|
|
81
|
-
val result = detector.advancedAntiSpoofing(
|
|
82
|
-
planes, image.width, image.height, rowStrides, pixelStrides
|
|
83
|
-
)
|
|
74
|
+
val rgba = extractRgbaBytes(image)
|
|
75
|
+
val result = detector.advancedAntiSpoofingRGBA(rgba, image.width, image.height)
|
|
84
76
|
val isLive = result.neuralNetworkScore < FaceAntiSpoofingAdvanced.BASE_THRESHOLD &&
|
|
85
77
|
result.laplacianScore > FaceAntiSpoofingAdvanced.LAPLACIAN_THRESHOLD
|
|
86
78
|
options.onResult.invoke(
|
|
@@ -101,6 +93,51 @@ class HybridSpoofDetectorOutput(
|
|
|
101
93
|
}
|
|
102
94
|
}
|
|
103
95
|
|
|
96
|
+
private fun extractRgbaBytes(image: ImageProxy): ByteArray {
|
|
97
|
+
val plane = image.planes.firstOrNull() ?: throw IllegalStateException("RGBA image has no planes")
|
|
98
|
+
val rowStride = plane.rowStride
|
|
99
|
+
val pixelStride = plane.pixelStride
|
|
100
|
+
val width = image.width
|
|
101
|
+
val height = image.height
|
|
102
|
+
val bytesPerPixel = 4
|
|
103
|
+
val rowBytes = width * bytesPerPixel
|
|
104
|
+
val rgba = ByteArray(rowBytes * height)
|
|
105
|
+
val buffer = plane.buffer.duplicate()
|
|
106
|
+
buffer.order(java.nio.ByteOrder.nativeOrder())
|
|
107
|
+
|
|
108
|
+
for (y in 0 until height) {
|
|
109
|
+
val srcOffset = y * rowStride
|
|
110
|
+
val dstOffset = y * rowBytes
|
|
111
|
+
val copyLength = rowBytes.coerceAtMost(rowStride)
|
|
112
|
+
buffer.position(srcOffset)
|
|
113
|
+
val slice = ByteArray(copyLength)
|
|
114
|
+
buffer.get(slice)
|
|
115
|
+
System.arraycopy(slice, 0, rgba, dstOffset, copyLength)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (pixelStride != 4) {
|
|
119
|
+
val packed = ByteArray(width * height * bytesPerPixel)
|
|
120
|
+
var outputIndex = 0
|
|
121
|
+
for (y in 0 until height) {
|
|
122
|
+
val rowOffset = y * rowStride
|
|
123
|
+
for (x in 0 until width) {
|
|
124
|
+
val pixelOffset = rowOffset + x * pixelStride
|
|
125
|
+
val r = buffer.get(pixelOffset).toInt() and 0xFF
|
|
126
|
+
val g = buffer.get(pixelOffset + 1).toInt() and 0xFF
|
|
127
|
+
val b = buffer.get(pixelOffset + 2).toInt() and 0xFF
|
|
128
|
+
val a = buffer.get(pixelOffset + 3).toInt() and 0xFF
|
|
129
|
+
packed[outputIndex++] = r.toByte()
|
|
130
|
+
packed[outputIndex++] = g.toByte()
|
|
131
|
+
packed[outputIndex++] = b.toByte()
|
|
132
|
+
packed[outputIndex++] = a.toByte()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return packed
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return rgba
|
|
139
|
+
}
|
|
140
|
+
|
|
104
141
|
override fun dispose() {
|
|
105
142
|
disposed.set(true)
|
|
106
143
|
imageAnalysis?.clearAnalyzer()
|
|
@@ -6,8 +6,17 @@ import TensorFlowLite
|
|
|
6
6
|
|
|
7
7
|
@objcMembers
|
|
8
8
|
public class FaceAntiSpoofManager: NSObject {
|
|
9
|
+
public struct DetectionResult {
|
|
10
|
+
let isLive: Bool
|
|
11
|
+
let neuralNetworkScore: Double
|
|
12
|
+
let laplacianScore: Double
|
|
13
|
+
let combinedScore: Double
|
|
14
|
+
let confidence: Double
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
#if canImport(TensorFlowLite)
|
|
10
18
|
public static var sharedInterpreter: Interpreter?
|
|
19
|
+
private static let interpreterLock = NSLock()
|
|
11
20
|
#endif
|
|
12
21
|
|
|
13
22
|
@objc public class func initializeModel() -> Bool {
|
|
@@ -15,11 +24,19 @@ public class FaceAntiSpoofManager: NSObject {
|
|
|
15
24
|
if sharedInterpreter != nil { return true }
|
|
16
25
|
|
|
17
26
|
// Look in main bundle then plugin bundle
|
|
18
|
-
var modelPath = Bundle.main.path(
|
|
27
|
+
var modelPath = Bundle.main.path(
|
|
28
|
+
forResource: "FaceAntiSpoofing",
|
|
29
|
+
ofType: "tflite",
|
|
30
|
+
inDirectory: "lib/assets"
|
|
31
|
+
) ?? Bundle.main.path(forResource: "FaceAntiSpoofing", ofType: "tflite")
|
|
19
32
|
if modelPath == nil {
|
|
20
|
-
if let
|
|
21
|
-
let podBundle = Bundle(for:
|
|
22
|
-
modelPath = podBundle.path(
|
|
33
|
+
if let managerClass = NSClassFromString("FaceAntiSpoofManager") {
|
|
34
|
+
let podBundle = Bundle(for: managerClass)
|
|
35
|
+
modelPath = podBundle.path(
|
|
36
|
+
forResource: "FaceAntiSpoofing",
|
|
37
|
+
ofType: "tflite",
|
|
38
|
+
inDirectory: "lib/assets"
|
|
39
|
+
) ?? podBundle.path(forResource: "FaceAntiSpoofing", ofType: "tflite")
|
|
23
40
|
}
|
|
24
41
|
}
|
|
25
42
|
|
|
@@ -54,6 +71,91 @@ public class FaceAntiSpoofManager: NSObject {
|
|
|
54
71
|
#endif
|
|
55
72
|
}
|
|
56
73
|
|
|
74
|
+
public class func process(input: [Float]) throws -> DetectionResult {
|
|
75
|
+
#if canImport(TensorFlowLite)
|
|
76
|
+
if sharedInterpreter == nil && !initializeModel() {
|
|
77
|
+
throw NSError(domain: "FaceAntiSpoof", code: 3, userInfo: [
|
|
78
|
+
NSLocalizedDescriptionKey: "Unable to initialize the TensorFlow Lite interpreter"
|
|
79
|
+
])
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interpreterLock.lock()
|
|
83
|
+
defer { interpreterLock.unlock() }
|
|
84
|
+
|
|
85
|
+
guard let interpreter = sharedInterpreter else {
|
|
86
|
+
throw NSError(domain: "FaceAntiSpoof", code: 1, userInfo: [
|
|
87
|
+
NSLocalizedDescriptionKey: "Interpreter not available"
|
|
88
|
+
])
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let inputData = input.withUnsafeBufferPointer { buffer in
|
|
92
|
+
Data(buffer: buffer)
|
|
93
|
+
}
|
|
94
|
+
try interpreter.copy(inputData, toInputAt: 0)
|
|
95
|
+
try interpreter.invoke()
|
|
96
|
+
|
|
97
|
+
let classOutput = try interpreter.output(at: 0).data.withUnsafeBytes { buffer in
|
|
98
|
+
Array(buffer.bindMemory(to: Float.self))
|
|
99
|
+
}
|
|
100
|
+
let maskOutput = try interpreter.output(at: 1).data.withUnsafeBytes { buffer in
|
|
101
|
+
Array(buffer.bindMemory(to: Float.self))
|
|
102
|
+
}
|
|
103
|
+
let count = min(classOutput.count, maskOutput.count)
|
|
104
|
+
let neuralScore = (0..<count).reduce(Float.zero) { total, index in
|
|
105
|
+
total + abs(classOutput[index]) * maskOutput[index]
|
|
106
|
+
}
|
|
107
|
+
let laplacianScore = calculateLaplacianScore(input)
|
|
108
|
+
let combinedScore = calculateCombinedScore(neuralScore, laplacianScore)
|
|
109
|
+
let confidence = combinedScore
|
|
110
|
+
let isLive = neuralScore < 0.2 && laplacianScore > 1000
|
|
111
|
+
|
|
112
|
+
return DetectionResult(
|
|
113
|
+
isLive: isLive,
|
|
114
|
+
neuralNetworkScore: Double(neuralScore),
|
|
115
|
+
laplacianScore: Double(laplacianScore),
|
|
116
|
+
combinedScore: Double(combinedScore),
|
|
117
|
+
confidence: Double(confidence)
|
|
118
|
+
)
|
|
119
|
+
#else
|
|
120
|
+
throw NSError(domain: "FaceAntiSpoof", code: 2, userInfo: [
|
|
121
|
+
NSLocalizedDescriptionKey: "TensorFlowLite is not available"
|
|
122
|
+
])
|
|
123
|
+
#endif
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private class func calculateLaplacianScore(_ input: [Float]) -> Int {
|
|
127
|
+
let width = 256
|
|
128
|
+
let height = 256
|
|
129
|
+
var grey = [Int](repeating: 0, count: width * height)
|
|
130
|
+
for index in 0..<(width * height) {
|
|
131
|
+
let offset = index * 3
|
|
132
|
+
grey[index] = Int(
|
|
133
|
+
(0.299 * input[offset] + 0.587 * input[offset + 1] + 0.114 * input[offset + 2]) * 255
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let kernel = [[0, 1, 0], [1, -4, 1], [0, 1, 0]]
|
|
138
|
+
var score = 0
|
|
139
|
+
for y in 0..<(height - 2) {
|
|
140
|
+
for x in 0..<(width - 2) {
|
|
141
|
+
var result = 0
|
|
142
|
+
for ky in 0..<3 {
|
|
143
|
+
for kx in 0..<3 {
|
|
144
|
+
result += grey[(y + ky) * width + x + kx] * kernel[ky][kx]
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if result > 50 { score += 1 }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return score
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private class func calculateCombinedScore(_ neuralScore: Float, _ laplacianScore: Int) -> Float {
|
|
154
|
+
let neural = min(max(neuralScore, 0), 1)
|
|
155
|
+
let laplacian = min(max(Float(laplacianScore) / 1000, 0), 1)
|
|
156
|
+
return ((1 - neural) * 0.6) + (laplacian * 0.4)
|
|
157
|
+
}
|
|
158
|
+
|
|
57
159
|
@objc public class func cleanupModel() {
|
|
58
160
|
#if canImport(TensorFlowLite)
|
|
59
161
|
sharedInterpreter = nil
|
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
#import "RCTBridge.h"
|
|
14
14
|
#endif
|
|
15
15
|
|
|
16
|
-
extern void FaceAntiSpoofRegisterPlugin(void);
|
|
17
|
-
|
|
18
16
|
@interface FaceAntiSpoofModule : NSObject <RCTBridgeModule>
|
|
19
17
|
@end
|
|
20
18
|
|
|
@@ -35,9 +33,7 @@ RCT_REMAP_METHOD(initialize,
|
|
|
35
33
|
initializeWithResolver:(RCTPromiseResolveBlock)resolve
|
|
36
34
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
37
35
|
{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
BOOL pluginAvailable = (NSClassFromString(@"FaceAntiSpoofFrameProcessor") != Nil);
|
|
36
|
+
BOOL pluginAvailable = (NSClassFromString(@"FaceAntiSpoofManager") != Nil);
|
|
41
37
|
|
|
42
38
|
// Prefer the FaceAntiSpoofManager to initialize the model and create the interpreter
|
|
43
39
|
BOOL managerInitialized = NO;
|
|
@@ -52,12 +48,18 @@ RCT_REMAP_METHOD(initialize,
|
|
|
52
48
|
}
|
|
53
49
|
|
|
54
50
|
// As a fallback, check bundle resources
|
|
55
|
-
NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
51
|
+
NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"FaceAntiSpoofing" ofType:@"tflite" inDirectory:@"lib/assets"];
|
|
52
|
+
if (!modelPath) {
|
|
53
|
+
modelPath = [[NSBundle mainBundle] pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
54
|
+
}
|
|
56
55
|
if (!modelPath) {
|
|
57
|
-
Class
|
|
58
|
-
if (
|
|
59
|
-
NSBundle *podBundle = [NSBundle bundleForClass:
|
|
60
|
-
modelPath = [podBundle pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
56
|
+
Class managerClass = NSClassFromString(@"FaceAntiSpoofManager");
|
|
57
|
+
if (managerClass != Nil) {
|
|
58
|
+
NSBundle *podBundle = [NSBundle bundleForClass:managerClass];
|
|
59
|
+
modelPath = [podBundle pathForResource:@"FaceAntiSpoofing" ofType:@"tflite" inDirectory:@"lib/assets"];
|
|
60
|
+
if (!modelPath) {
|
|
61
|
+
modelPath = [podBundle pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
62
|
+
}
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
BOOL modelLoaded = (modelPath != nil) || managerInitialized;
|
|
@@ -69,14 +71,19 @@ RCT_REMAP_METHOD(checkModelStatus,
|
|
|
69
71
|
checkModelStatusWithResolver:(RCTPromiseResolveBlock)resolve
|
|
70
72
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
71
73
|
{
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
74
|
+
BOOL pluginAvailable = (NSClassFromString(@"FaceAntiSpoofManager") != Nil);
|
|
75
|
+
NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"FaceAntiSpoofing" ofType:@"tflite" inDirectory:@"lib/assets"];
|
|
75
76
|
if (!modelPath) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
modelPath = [[NSBundle mainBundle] pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
78
|
+
}
|
|
79
|
+
if (!modelPath) {
|
|
80
|
+
Class managerClass = NSClassFromString(@"FaceAntiSpoofManager");
|
|
81
|
+
if (managerClass != Nil) {
|
|
82
|
+
NSBundle *podBundle = [NSBundle bundleForClass:managerClass];
|
|
83
|
+
modelPath = [podBundle pathForResource:@"FaceAntiSpoofing" ofType:@"tflite" inDirectory:@"lib/assets"];
|
|
84
|
+
if (!modelPath) {
|
|
85
|
+
modelPath = [podBundle pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
86
|
+
}
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
BOOL modelLoaded = (modelPath != nil);
|
|
@@ -88,7 +95,7 @@ RCT_REMAP_METHOD(isAvailable,
|
|
|
88
95
|
isAvailableWithResolver:(RCTPromiseResolveBlock)resolve
|
|
89
96
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
90
97
|
{
|
|
91
|
-
BOOL pluginAvailable = (NSClassFromString(@"
|
|
98
|
+
BOOL pluginAvailable = (NSClassFromString(@"FaceAntiSpoofManager") != Nil);
|
|
92
99
|
resolve(@(pluginAvailable));
|
|
93
100
|
}
|
|
94
101
|
|
|
@@ -96,7 +103,7 @@ RCT_REMAP_METHOD(testMethod,
|
|
|
96
103
|
testMethodWithResolver:(RCTPromiseResolveBlock)resolve
|
|
97
104
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
98
105
|
{
|
|
99
|
-
resolve(@"iOS native module
|
|
106
|
+
resolve(@"iOS native module is working!");
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
RCT_REMAP_METHOD(getModuleInfo,
|
|
@@ -111,7 +118,6 @@ RCT_REMAP_METHOD(install,
|
|
|
111
118
|
installWithResolver:(RCTPromiseResolveBlock)resolve
|
|
112
119
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
113
120
|
{
|
|
114
|
-
FaceAntiSpoofRegisterPlugin();
|
|
115
121
|
// Attempt to initialize shared model resources via FaceAntiSpoofManager
|
|
116
122
|
BOOL managerInitialized = NO;
|
|
117
123
|
Class managerClass = NSClassFromString(@"FaceAntiSpoofManager");
|
|
@@ -123,41 +129,22 @@ RCT_REMAP_METHOD(install,
|
|
|
123
129
|
}
|
|
124
130
|
}
|
|
125
131
|
|
|
126
|
-
BOOL pluginAvailable = (NSClassFromString(@"
|
|
127
|
-
NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
132
|
+
BOOL pluginAvailable = (NSClassFromString(@"FaceAntiSpoofManager") != Nil);
|
|
133
|
+
NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"FaceAntiSpoofing" ofType:@"tflite" inDirectory:@"lib/assets"];
|
|
128
134
|
if (!modelPath) {
|
|
129
|
-
|
|
130
|
-
if (fpsClass != Nil) {
|
|
131
|
-
NSBundle *podBundle = [NSBundle bundleForClass:fpsClass];
|
|
132
|
-
modelPath = [podBundle pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
133
|
-
}
|
|
135
|
+
modelPath = [[NSBundle mainBundle] pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
134
136
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
@try {
|
|
144
|
-
runtimePtr = [_faceAntiSpoofBridge valueForKeyPath:@"jsContextRef"];
|
|
145
|
-
} @catch (NSException *_) {}
|
|
146
|
-
}
|
|
147
|
-
if (!runtimePtr) {
|
|
148
|
-
@try {
|
|
149
|
-
runtimePtr = [_faceAntiSpoofBridge valueForKeyPath:@"javaScriptContextRef"];
|
|
150
|
-
} @catch (NSException *_) {}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (runtimePtr) {
|
|
154
|
-
// Pass the pointer to the JSI install stub
|
|
155
|
-
void *ptr = (__bridge void *)(runtimePtr);
|
|
156
|
-
extern void FaceAntiSpoofJSI_install(void *runtimePtr);
|
|
157
|
-
FaceAntiSpoofJSI_install(ptr);
|
|
137
|
+
if (!modelPath) {
|
|
138
|
+
Class managerClass = NSClassFromString(@"FaceAntiSpoofManager");
|
|
139
|
+
if (managerClass != Nil) {
|
|
140
|
+
NSBundle *podBundle = [NSBundle bundleForClass:managerClass];
|
|
141
|
+
modelPath = [podBundle pathForResource:@"FaceAntiSpoofing" ofType:@"tflite" inDirectory:@"lib/assets"];
|
|
142
|
+
if (!modelPath) {
|
|
143
|
+
modelPath = [podBundle pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
144
|
+
}
|
|
158
145
|
}
|
|
159
146
|
}
|
|
160
|
-
|
|
147
|
+
BOOL modelLoaded = (modelPath != nil) || managerInitialized;
|
|
161
148
|
resolve(@(pluginAvailable && modelLoaded));
|
|
162
149
|
}
|
|
163
150
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import NitroModules
|
|
2
|
+
import VisionCamera
|
|
3
|
+
|
|
4
|
+
final class HybridSpoofDetectorFactory: HybridSpoofDetectorFactorySpec {
|
|
5
|
+
func createSpoofDetectorOutput(options: SpoofDetectorOutputOptions) throws
|
|
6
|
+
-> any HybridCameraOutputSpec
|
|
7
|
+
{
|
|
8
|
+
return HybridSpoofDetectorOutput(options: options)
|
|
9
|
+
}
|
|
10
|
+
}
|