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.
Files changed (32) hide show
  1. package/VisionCameraSpoofDetector.podspec +41 -0
  2. package/android/src/main/java/com/faceantispoof/FaceAntiSpoofingAdvanced.kt +172 -0
  3. package/android/src/main/java/com/margelo/nitro/camera/spoofdetector/HybridSpoofDetectorOutput.kt +48 -11
  4. package/ios/FaceAntiSpoofManager.swift +106 -4
  5. package/ios/FaceAntiSpoofModule.m +38 -51
  6. package/ios/HybridSpoofDetectorFactory.swift +10 -0
  7. package/ios/HybridSpoofDetectorOutput.swift +164 -0
  8. package/nitro.json +4 -0
  9. package/nitrogen/generated/ios/VisionCameraSpoofDetector+autolinking.rb +62 -0
  10. package/nitrogen/generated/ios/VisionCameraSpoofDetector-Swift-Cxx-Bridge.cpp +60 -0
  11. package/nitrogen/generated/ios/VisionCameraSpoofDetector-Swift-Cxx-Bridge.hpp +151 -0
  12. package/nitrogen/generated/ios/VisionCameraSpoofDetector-Swift-Cxx-Umbrella.hpp +60 -0
  13. package/nitrogen/generated/ios/VisionCameraSpoofDetectorAutolinking.mm +33 -0
  14. package/nitrogen/generated/ios/VisionCameraSpoofDetectorAutolinking.swift +26 -0
  15. package/nitrogen/generated/ios/c++/HybridSpoofDetectorFactorySpecSwift.cpp +11 -0
  16. package/nitrogen/generated/ios/c++/HybridSpoofDetectorFactorySpecSwift.hpp +97 -0
  17. package/nitrogen/generated/ios/swift/AntiSpoofingResult.swift +54 -0
  18. package/nitrogen/generated/ios/swift/Func_void_AntiSpoofingResult.swift +46 -0
  19. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  20. package/nitrogen/generated/ios/swift/HybridSpoofDetectorFactorySpec.swift +56 -0
  21. package/nitrogen/generated/ios/swift/HybridSpoofDetectorFactorySpec_cxx.swift +142 -0
  22. package/nitrogen/generated/ios/swift/Size.swift +34 -0
  23. package/nitrogen/generated/ios/swift/SpoofDetectorOutputOptions.swift +74 -0
  24. package/package.json +8 -3
  25. package/src/specs/SpoofDetectorFactory.nitro.ts +1 -0
  26. package/ios/FaceAntiSpoofFrameProcessor.swift +0 -301
  27. package/ios/FaceAntiSpoofJSI.h +0 -12
  28. package/ios/FaceAntiSpoofJSI.mm +0 -92
  29. package/ios/FaceAntiSpoofPluginRegister.m +0 -84
  30. package/ios/models/FaceAntiSpoofing.tflite +0 -0
  31. package/react-native-vision-camera-spoof-detector.podspec +0 -22
  32. /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 {
@@ -53,7 +53,7 @@ class HybridSpoofDetectorOutput(
53
53
  .build()
54
54
  }
55
55
  val analysis = ImageAnalysis.Builder()
56
- .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
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 sourcePlanes = image.planes
75
- val planes = Array(3) { index ->
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(forResource: "FaceAntiSpoofing", ofType: "tflite")
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 fpsClass = NSClassFromString("FaceAntiSpoofFrameProcessor") as? AnyClass {
21
- let podBundle = Bundle(for: fpsClass)
22
- modelPath = podBundle.path(forResource: "FaceAntiSpoofing", ofType: "tflite")
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
- FaceAntiSpoofRegisterPlugin();
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 fpsClass = NSClassFromString(@"FaceAntiSpoofFrameProcessor");
58
- if (fpsClass != Nil) {
59
- NSBundle *podBundle = [NSBundle bundleForClass:fpsClass];
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
- FaceAntiSpoofRegisterPlugin();
73
- BOOL pluginAvailable = (NSClassFromString(@"FaceAntiSpoofFrameProcessor") != Nil);
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
- Class fpsClass = NSClassFromString(@"FaceAntiSpoofFrameProcessor");
77
- if (fpsClass != Nil) {
78
- NSBundle *podBundle = [NSBundle bundleForClass:fpsClass];
79
- modelPath = [podBundle pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
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(@"FaceAntiSpoofFrameProcessor") != Nil);
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 stub");
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(@"FaceAntiSpoofFrameProcessor") != Nil);
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
- Class fpsClass = NSClassFromString(@"FaceAntiSpoofFrameProcessor");
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
- BOOL modelLoaded = (modelPath != nil) || managerInitialized;
136
- // Attempt to register JSI: try common bridge keys to obtain a runtime pointer
137
- if (_faceAntiSpoofBridge != nil) {
138
- id runtimePtr = nil;
139
- @try {
140
- runtimePtr = [_faceAntiSpoofBridge valueForKeyPath:@"jsContext"];
141
- } @catch (NSException *_) {}
142
- if (!runtimePtr) {
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
+ }