react-native-vision-camera-spoof-detector 1.0.28 → 1.1.7
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.
Potentially problematic release.
This version of react-native-vision-camera-spoof-detector might be problematic. Click here for more details.
- package/.npmignore +16 -99
- package/README.md +5 -4
- package/android/CMakeLists.txt +29 -0
- package/android/build.gradle +26 -12
- package/android/fix-prefab.gradle +4 -0
- package/android/src/main/AndroidManifest.xml +1 -1
- package/android/src/main/cpp/CMakeLists.txt +18 -86
- package/android/src/main/cpp/FaceAntiSpoofJSI.cpp +80 -6
- package/android/src/main/cpp/cpp-adapter.cpp +9 -0
- package/android/src/main/java/com/faceantispoof/FaceAntiSpoofModule.kt +108 -103
- package/android/src/main/java/com/faceantispoof/FaceAntiSpoofPackage.kt +12 -27
- package/android/src/main/java/com/faceantispoof/FaceAntiSpoofingAdvanced.kt +58 -1
- package/android/src/main/java/com/margelo/nitro/camera/spoofdetector/HybridSpoofDetectorFactory.kt +15 -0
- package/android/src/main/java/com/margelo/nitro/camera/spoofdetector/HybridSpoofDetectorOutput.kt +107 -0
- package/index.d.ts +14 -1
- package/index.js +26 -57
- package/nitro.json +20 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/VisionCameraSpoofDetector+autolinking.cmake +81 -0
- package/nitrogen/generated/android/VisionCameraSpoofDetector+autolinking.gradle +27 -0
- package/nitrogen/generated/android/VisionCameraSpoofDetectorOnLoad.cpp +58 -0
- package/nitrogen/generated/android/VisionCameraSpoofDetectorOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JAntiSpoofingResult.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_AntiSpoofingResult.hpp +78 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__exception_ptr.hpp +76 -0
- package/nitrogen/generated/android/c++/JHybridSpoofDetectorFactorySpec.cpp +75 -0
- package/nitrogen/generated/android/c++/JHybridSpoofDetectorFactorySpec.hpp +63 -0
- package/nitrogen/generated/android/c++/JSize.hpp +61 -0
- package/nitrogen/generated/android/c++/JSpoofDetectorOutputOptions.hpp +91 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/spoofdetector/AntiSpoofingResult.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/spoofdetector/Func_void_AntiSpoofingResult.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/spoofdetector/Func_void_std__exception_ptr.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/spoofdetector/HybridSpoofDetectorFactorySpec.kt +57 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/spoofdetector/Size.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/spoofdetector/SpoofDetectorOutputOptions.kt +65 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/spoofdetector/VisionCameraSpoofDetectorOnLoad.kt +35 -0
- package/nitrogen/generated/shared/c++/AntiSpoofingResult.hpp +103 -0
- package/nitrogen/generated/shared/c++/HybridSpoofDetectorFactorySpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridSpoofDetectorFactorySpec.hpp +67 -0
- package/nitrogen/generated/shared/c++/Size.hpp +87 -0
- package/nitrogen/generated/shared/c++/SpoofDetectorOutputOptions.hpp +98 -0
- package/package.json +12 -13
- package/react-native-vision-camera-spoof-detector.podspec +4 -4
- package/src/specs/SpoofDetectorFactory.nitro.ts +23 -0
- package/.gitattributes +0 -2
- package/CONTRIBUTING.md +0 -280
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
- package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.9/gc.properties +0 -0
- package/android/.gradle/9.2.0/checksums/checksums.lock +0 -0
- package/android/.gradle/9.2.0/checksums/md5-checksums.bin +0 -0
- package/android/.gradle/9.2.0/checksums/sha1-checksums.bin +0 -0
- package/android/.gradle/9.2.0/fileChanges/last-build.bin +0 -0
- package/android/.gradle/9.2.0/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/9.2.0/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/9.2.0/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build/reports/problems/problems-report.html +0 -659
- package/android/src/main/assets/spoof_model_scale_2_7.tflite +0 -0
- package/android/src/main/assets/spoof_model_scale_4_0.tflite +0 -0
- package/android/src/main/java/com/faceantispoof/FaceAntiSpoofFrameProcessor.kt +0 -198
- /package/{android/src/main → lib}/assets/FaceAntiSpoofing.tflite +0 -0
|
@@ -4,6 +4,8 @@ import com.facebook.react.bridge.ReactApplicationContext
|
|
|
4
4
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
5
5
|
import com.facebook.react.bridge.ReactMethod
|
|
6
6
|
import com.facebook.react.bridge.Promise
|
|
7
|
+
import com.facebook.react.bridge.Arguments
|
|
8
|
+
import com.facebook.react.bridge.ReadableArray
|
|
7
9
|
import com.facebook.react.module.annotations.ReactModule
|
|
8
10
|
|
|
9
11
|
@ReactModule(name = FaceAntiSpoofModule.NAME)
|
|
@@ -11,17 +13,27 @@ class FaceAntiSpoofModule(reactContext: ReactApplicationContext) : ReactContextB
|
|
|
11
13
|
|
|
12
14
|
companion object {
|
|
13
15
|
const val NAME = "FaceAntiSpoof"
|
|
16
|
+
private var activeModule: FaceAntiSpoofModule? = null
|
|
17
|
+
|
|
18
|
+
@JvmStatic
|
|
19
|
+
fun processFrameFromJSI(
|
|
20
|
+
planes: Array<ByteArray>,
|
|
21
|
+
width: Int,
|
|
22
|
+
height: Int,
|
|
23
|
+
rowStrides: IntArray,
|
|
24
|
+
pixelStrides: IntArray
|
|
25
|
+
): DoubleArray {
|
|
26
|
+
val module = activeModule
|
|
27
|
+
?: return doubleArrayOf(0.0, 0.0, 0.0, 0.0, 0.0)
|
|
28
|
+
return module.processNativeFrame(planes, width, height, rowStrides, pixelStrides)
|
|
29
|
+
}
|
|
14
30
|
}
|
|
15
31
|
|
|
16
32
|
private var faceAntiSpoofing: FaceAntiSpoofingAdvanced? = null
|
|
17
33
|
private var isModuleInitialized = false
|
|
18
34
|
|
|
19
35
|
init {
|
|
20
|
-
|
|
21
|
-
System.loadLibrary("faceantispoof")
|
|
22
|
-
} catch (e: Exception) {
|
|
23
|
-
android.util.Log.e("FaceAntiSpoof", "Error loading native library", e)
|
|
24
|
-
}
|
|
36
|
+
activeModule = this
|
|
25
37
|
|
|
26
38
|
try {
|
|
27
39
|
// Initialize face anti-spoofing with app assets
|
|
@@ -33,57 +45,6 @@ class FaceAntiSpoofModule(reactContext: ReactApplicationContext) : ReactContextB
|
|
|
33
45
|
isModuleInitialized = false
|
|
34
46
|
}
|
|
35
47
|
|
|
36
|
-
try {
|
|
37
|
-
tryRegisterJSI()
|
|
38
|
-
} catch (e: Exception) {
|
|
39
|
-
android.util.Log.e("FaceAntiSpoof", "Error starting JSI registration", e)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
private fun tryRegisterJSI() {
|
|
44
|
-
try {
|
|
45
|
-
Thread {
|
|
46
|
-
try {
|
|
47
|
-
Thread.sleep(500)
|
|
48
|
-
val holder = reactApplicationContext.javaScriptContextHolder ?: return@Thread
|
|
49
|
-
|
|
50
|
-
// Try nativePointer
|
|
51
|
-
try {
|
|
52
|
-
val field = holder.javaClass.getDeclaredField("nativePointer")
|
|
53
|
-
field.isAccessible = true
|
|
54
|
-
val jsContextPtr = field.getLong(holder)
|
|
55
|
-
if (jsContextPtr != 0L) {
|
|
56
|
-
try {
|
|
57
|
-
registerJSIFunction(jsContextPtr)
|
|
58
|
-
} catch (e: Exception) {
|
|
59
|
-
android.util.Log.d("FaceAntiSpoof", "JSI registration not available: ${e.message}")
|
|
60
|
-
}
|
|
61
|
-
return@Thread
|
|
62
|
-
}
|
|
63
|
-
} catch (_: Exception) { }
|
|
64
|
-
|
|
65
|
-
// Try mContext
|
|
66
|
-
try {
|
|
67
|
-
val field = holder.javaClass.getDeclaredField("mContext")
|
|
68
|
-
field.isAccessible = true
|
|
69
|
-
val jsContextPtr = field.getLong(holder)
|
|
70
|
-
if (jsContextPtr != 0L) {
|
|
71
|
-
try {
|
|
72
|
-
registerJSIFunction(jsContextPtr)
|
|
73
|
-
} catch (e: Exception) {
|
|
74
|
-
android.util.Log.d("FaceAntiSpoof", "JSI registration not available: ${e.message}")
|
|
75
|
-
}
|
|
76
|
-
return@Thread
|
|
77
|
-
}
|
|
78
|
-
} catch (_: Exception) { }
|
|
79
|
-
|
|
80
|
-
} catch (e: Exception) {
|
|
81
|
-
android.util.Log.d("FaceAntiSpoof", "Error in JSI registration thread: ${e.message}")
|
|
82
|
-
}
|
|
83
|
-
}.start()
|
|
84
|
-
} catch (e: Exception) {
|
|
85
|
-
android.util.Log.d("FaceAntiSpoof", "Failed to start thread for JSI registration: ${e.message}")
|
|
86
|
-
}
|
|
87
48
|
}
|
|
88
49
|
|
|
89
50
|
override fun getName(): String = NAME
|
|
@@ -126,6 +87,94 @@ class FaceAntiSpoofModule(reactContext: ReactApplicationContext) : ReactContextB
|
|
|
126
87
|
}
|
|
127
88
|
}
|
|
128
89
|
|
|
90
|
+
@ReactMethod
|
|
91
|
+
fun processFrame(
|
|
92
|
+
frameData: ReadableArray,
|
|
93
|
+
width: Int,
|
|
94
|
+
height: Int,
|
|
95
|
+
rowStrides: ReadableArray,
|
|
96
|
+
pixelStrides: ReadableArray,
|
|
97
|
+
promise: Promise
|
|
98
|
+
) {
|
|
99
|
+
try {
|
|
100
|
+
if (frameData.size() < 3 || rowStrides.size() < 3 || pixelStrides.size() < 3) {
|
|
101
|
+
promise.reject("INVALID_FRAME", "Expected Y, U and V camera planes")
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!isModuleInitialized || faceAntiSpoofing == null) {
|
|
106
|
+
promise.reject("NOT_INITIALIZED", "Face anti-spoofing is not initialized")
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
val planes = Array(3) { planeIndex ->
|
|
111
|
+
val plane = frameData.getArray(planeIndex)
|
|
112
|
+
?: throw IllegalArgumentException("Camera plane $planeIndex is null")
|
|
113
|
+
ByteArray(plane.size()) { index -> plane.getInt(index).toByte() }
|
|
114
|
+
}
|
|
115
|
+
val yuvRowStrides = IntArray(3) { rowStrides.getDouble(it).toInt() }
|
|
116
|
+
val yuvPixelStrides = IntArray(3) { pixelStrides.getDouble(it).toInt().coerceAtLeast(1) }
|
|
117
|
+
val result = faceAntiSpoofing!!.advancedAntiSpoofing(
|
|
118
|
+
planes,
|
|
119
|
+
width,
|
|
120
|
+
height,
|
|
121
|
+
yuvRowStrides,
|
|
122
|
+
yuvPixelStrides
|
|
123
|
+
)
|
|
124
|
+
val isLive = result.neuralNetworkScore < FaceAntiSpoofingAdvanced.BASE_THRESHOLD &&
|
|
125
|
+
result.laplacianScore > FaceAntiSpoofingAdvanced.LAPLACIAN_THRESHOLD
|
|
126
|
+
promise.resolve(
|
|
127
|
+
Arguments.makeNativeMap(
|
|
128
|
+
mapOf(
|
|
129
|
+
"isLive" to isLive,
|
|
130
|
+
"label" to if (isLive) "Live Face" else "Spoof Face",
|
|
131
|
+
"neuralNetworkScore" to result.neuralNetworkScore.toDouble(),
|
|
132
|
+
"laplacianScore" to result.laplacianScore,
|
|
133
|
+
"combinedScore" to result.combinedScore.toDouble(),
|
|
134
|
+
"confidence" to result.confidence.toDouble()
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
} catch (e: Exception) {
|
|
139
|
+
promise.reject("PROCESS_ERROR", "Failed to process camera frame", e)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@Synchronized
|
|
144
|
+
private fun processNativeFrame(
|
|
145
|
+
planes: Array<ByteArray>,
|
|
146
|
+
width: Int,
|
|
147
|
+
height: Int,
|
|
148
|
+
rowStrides: IntArray,
|
|
149
|
+
pixelStrides: IntArray
|
|
150
|
+
): DoubleArray {
|
|
151
|
+
if (!isModuleInitialized || faceAntiSpoofing == null || planes.size < 3) {
|
|
152
|
+
return doubleArrayOf(0.0, 0.0, 0.0, 0.0, 0.0)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return try {
|
|
156
|
+
val result = faceAntiSpoofing!!.advancedAntiSpoofing(
|
|
157
|
+
planes,
|
|
158
|
+
width,
|
|
159
|
+
height,
|
|
160
|
+
rowStrides,
|
|
161
|
+
pixelStrides
|
|
162
|
+
)
|
|
163
|
+
val isLive = result.neuralNetworkScore < FaceAntiSpoofingAdvanced.BASE_THRESHOLD &&
|
|
164
|
+
result.laplacianScore > FaceAntiSpoofingAdvanced.LAPLACIAN_THRESHOLD
|
|
165
|
+
doubleArrayOf(
|
|
166
|
+
if (isLive) 1.0 else 0.0,
|
|
167
|
+
result.neuralNetworkScore.toDouble(),
|
|
168
|
+
result.laplacianScore.toDouble(),
|
|
169
|
+
result.combinedScore.toDouble(),
|
|
170
|
+
result.confidence.toDouble()
|
|
171
|
+
)
|
|
172
|
+
} catch (error: Exception) {
|
|
173
|
+
android.util.Log.e("FaceAntiSpoof", "Native frame processing failed", error)
|
|
174
|
+
doubleArrayOf(0.0, 0.0, 0.0, 0.0, 0.0)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
129
178
|
@ReactMethod
|
|
130
179
|
fun isAvailable(promise: Promise) {
|
|
131
180
|
try {
|
|
@@ -163,51 +212,9 @@ class FaceAntiSpoofModule(reactContext: ReactApplicationContext) : ReactContextB
|
|
|
163
212
|
|
|
164
213
|
@ReactMethod
|
|
165
214
|
fun install(promise: Promise) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (holder != null) {
|
|
170
|
-
// Try nativePointer
|
|
171
|
-
try {
|
|
172
|
-
val f = holder.javaClass.getDeclaredField("nativePointer")
|
|
173
|
-
f.isAccessible = true
|
|
174
|
-
val ptr = f.getLong(holder)
|
|
175
|
-
if (ptr != 0L) {
|
|
176
|
-
try {
|
|
177
|
-
registerJSIFunction(ptr)
|
|
178
|
-
promise.resolve(true)
|
|
179
|
-
return
|
|
180
|
-
} catch (e: Exception) {
|
|
181
|
-
android.util.Log.d("FaceAntiSpoof", "JSI not available: ${e.message}")
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
} catch (_: Exception) {}
|
|
185
|
-
|
|
186
|
-
// Try mContext
|
|
187
|
-
try {
|
|
188
|
-
val f = holder.javaClass.getDeclaredField("mContext")
|
|
189
|
-
f.isAccessible = true
|
|
190
|
-
val ptr = f.getLong(holder)
|
|
191
|
-
if (ptr != 0L) {
|
|
192
|
-
try {
|
|
193
|
-
registerJSIFunction(ptr)
|
|
194
|
-
promise.resolve(true)
|
|
195
|
-
return
|
|
196
|
-
} catch (e: Exception) {
|
|
197
|
-
android.util.Log.d("FaceAntiSpoof", "JSI not available: ${e.message}")
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
} catch (_: Exception) {}
|
|
201
|
-
|
|
202
|
-
promise.resolve(false)
|
|
203
|
-
} else {
|
|
204
|
-
promise.resolve(false)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
} catch (e: Exception) {
|
|
208
|
-
android.util.Log.e("FaceAntiSpoof", "install() error", e)
|
|
209
|
-
promise.reject("INSTALL_ERROR", "Failed to install JSI function", e)
|
|
210
|
-
}
|
|
215
|
+
// VisionCamera 5 uses the Nitro CameraOutput. The old reflective JSI
|
|
216
|
+
// installer targets a native library that is no longer part of this module.
|
|
217
|
+
promise.resolve(false)
|
|
211
218
|
}
|
|
212
219
|
|
|
213
220
|
@ReactMethod
|
|
@@ -237,6 +244,4 @@ class FaceAntiSpoofModule(reactContext: ReactApplicationContext) : ReactContextB
|
|
|
237
244
|
super.onCatalystInstanceDestroy()
|
|
238
245
|
}
|
|
239
246
|
|
|
240
|
-
// Native JSI registration - optional, may not be available
|
|
241
|
-
private external fun registerJSIFunction(jsContext: Long)
|
|
242
247
|
}
|
|
@@ -1,37 +1,22 @@
|
|
|
1
|
-
package com.
|
|
1
|
+
package com.margelo.nitro.camera.spoofdetector
|
|
2
2
|
|
|
3
|
-
import com.facebook.react.
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
4
|
import com.facebook.react.bridge.NativeModule
|
|
5
5
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
-
import com.facebook.react.
|
|
7
|
-
import com.
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
7
|
+
import com.faceantispoof.FaceAntiSpoofModule
|
|
8
8
|
|
|
9
|
-
class FaceAntiSpoofPackage :
|
|
9
|
+
class FaceAntiSpoofPackage : BaseReactPackage() {
|
|
10
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
|
|
11
|
+
if (name == FaceAntiSpoofModule.NAME) FaceAntiSpoofModule(reactContext) else null
|
|
12
|
+
|
|
13
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
|
|
14
|
+
HashMap()
|
|
15
|
+
}
|
|
10
16
|
|
|
11
17
|
companion object {
|
|
12
18
|
init {
|
|
13
|
-
|
|
14
|
-
FrameProcessorPluginRegistry.addFrameProcessorPlugin("faceAntiSpoof") { proxy, options ->
|
|
15
|
-
val plugin = FaceAntiSpoofFrameProcessor()
|
|
16
|
-
try {
|
|
17
|
-
plugin.initialize(proxy.context.assets)
|
|
18
|
-
} catch (e: Exception) {
|
|
19
|
-
android.util.Log.e("FaceAntiSpoof", "Failed to initialize plugin with assets", e)
|
|
20
|
-
}
|
|
21
|
-
plugin
|
|
22
|
-
}
|
|
23
|
-
} catch (e: Exception) {
|
|
24
|
-
android.util.Log.e("FaceAntiSpoof", "Plugin already registered or failed: ${e.message}", e)
|
|
25
|
-
}
|
|
19
|
+
VisionCameraSpoofDetectorOnLoad.initializeNative()
|
|
26
20
|
}
|
|
27
21
|
}
|
|
28
|
-
|
|
29
|
-
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
30
|
-
// Only return the module — DO NOT register plugin here.
|
|
31
|
-
return listOf(FaceAntiSpoofModule(reactContext))
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
35
|
-
return emptyList()
|
|
36
|
-
}
|
|
37
22
|
}
|
|
@@ -24,7 +24,7 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
24
24
|
const val BASE_THRESHOLD = 0.2f
|
|
25
25
|
const val ROUTE_INDEX = 6
|
|
26
26
|
const val LAPLACE_THRESHOLD = 50
|
|
27
|
-
const val LAPLACIAN_THRESHOLD =
|
|
27
|
+
const val LAPLACIAN_THRESHOLD = 1000
|
|
28
28
|
const val MODEL_FILE = "FaceAntiSpoofing.tflite"
|
|
29
29
|
private const val TAG = "FaceAntiSpoofing"
|
|
30
30
|
|
|
@@ -165,6 +165,25 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
fun advancedAntiSpoofing(
|
|
169
|
+
planes: Array<ByteArray>,
|
|
170
|
+
width: Int,
|
|
171
|
+
height: Int,
|
|
172
|
+
rowStrides: IntArray,
|
|
173
|
+
pixelStrides: IntArray
|
|
174
|
+
): AntiSpoofingResult {
|
|
175
|
+
if (planes.size < 3) {
|
|
176
|
+
throw IllegalArgumentException("Expected Y, U and V planes")
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
val bitmap = yuv420ToBitmap(planes, width, height, rowStrides, pixelStrides)
|
|
180
|
+
return try {
|
|
181
|
+
runBlocking { advancedAntiSpoofing(bitmap).getOrThrow() }
|
|
182
|
+
} finally {
|
|
183
|
+
bitmap.recycle()
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
168
187
|
// FROM WORKING VERSION: neuralNetworkAntiSpoofing with Bitmap
|
|
169
188
|
private fun neuralNetworkAntiSpoofing(bitmap: Bitmap): Result<Float> {
|
|
170
189
|
return try {
|
|
@@ -365,6 +384,44 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
365
384
|
return bitmap
|
|
366
385
|
}
|
|
367
386
|
|
|
387
|
+
private fun yuv420ToBitmap(
|
|
388
|
+
planes: Array<ByteArray>,
|
|
389
|
+
width: Int,
|
|
390
|
+
height: Int,
|
|
391
|
+
rowStrides: IntArray,
|
|
392
|
+
pixelStrides: IntArray
|
|
393
|
+
): Bitmap {
|
|
394
|
+
val pixels = IntArray(width * height)
|
|
395
|
+
val yPlane = planes[0]
|
|
396
|
+
val uPlane = planes[1]
|
|
397
|
+
val vPlane = planes[2]
|
|
398
|
+
|
|
399
|
+
fun sample(plane: ByteArray, row: Int, column: Int, rowStride: Int, pixelStride: Int): Int {
|
|
400
|
+
val index = row * rowStride + column * pixelStride
|
|
401
|
+
return if (index in plane.indices) plane[index].toInt() and 0xFF else 128
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
for (y in 0 until height) {
|
|
405
|
+
for (x in 0 until width) {
|
|
406
|
+
val yValue = sample(yPlane, y, x, rowStrides[0], pixelStrides[0])
|
|
407
|
+
val chromaRow = y / 2
|
|
408
|
+
val chromaColumn = x / 2
|
|
409
|
+
val uValue = sample(uPlane, chromaRow, chromaColumn, rowStrides[1], pixelStrides[1]) - 128
|
|
410
|
+
val vValue = sample(vPlane, chromaRow, chromaColumn, rowStrides[2], pixelStrides[2]) - 128
|
|
411
|
+
|
|
412
|
+
val red = (yValue + 1.402f * vValue).coerceIn(0f, 255f).toInt()
|
|
413
|
+
val green = (yValue - 0.344f * uValue - 0.714f * vValue).coerceIn(0f, 255f).toInt()
|
|
414
|
+
val blue = (yValue + 1.772f * uValue).coerceIn(0f, 255f).toInt()
|
|
415
|
+
pixels[y * width + x] = (0xFF shl 24) or
|
|
416
|
+
(red shl 16) or
|
|
417
|
+
(green shl 8) or
|
|
418
|
+
blue
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888)
|
|
423
|
+
}
|
|
424
|
+
|
|
368
425
|
@Throws(IOException::class)
|
|
369
426
|
private fun loadModelFile(assetManager: AssetManager, modelName: String): MappedByteBuffer {
|
|
370
427
|
return try {
|
package/android/src/main/java/com/margelo/nitro/camera/spoofdetector/HybridSpoofDetectorFactory.kt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
package com.margelo.nitro.camera.spoofdetector
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.Keep
|
|
4
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
5
|
+
import com.margelo.nitro.camera.HybridCameraOutputSpec
|
|
6
|
+
|
|
7
|
+
@DoNotStrip
|
|
8
|
+
@Keep
|
|
9
|
+
class HybridSpoofDetectorFactory : HybridSpoofDetectorFactorySpec() {
|
|
10
|
+
@DoNotStrip
|
|
11
|
+
@Keep
|
|
12
|
+
override fun createSpoofDetectorOutput(options: SpoofDetectorOutputOptions): HybridCameraOutputSpec {
|
|
13
|
+
return HybridSpoofDetectorOutput(options)
|
|
14
|
+
}
|
|
15
|
+
}
|
package/android/src/main/java/com/margelo/nitro/camera/spoofdetector/HybridSpoofDetectorOutput.kt
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
package com.margelo.nitro.camera.spoofdetector
|
|
2
|
+
|
|
3
|
+
import androidx.camera.core.ImageAnalysis
|
|
4
|
+
import androidx.camera.core.ImageProxy
|
|
5
|
+
import androidx.camera.core.resolutionselector.ResolutionSelector
|
|
6
|
+
import androidx.camera.core.resolutionselector.ResolutionStrategy
|
|
7
|
+
import com.margelo.nitro.NitroModules
|
|
8
|
+
import com.margelo.nitro.camera.CameraOrientation
|
|
9
|
+
import com.margelo.nitro.camera.HybridCameraOutputSpec
|
|
10
|
+
import com.margelo.nitro.camera.MediaType
|
|
11
|
+
import com.margelo.nitro.camera.MirrorMode
|
|
12
|
+
import com.margelo.nitro.camera.extensions.setAllowDroppingLateFrames
|
|
13
|
+
import com.margelo.nitro.camera.extensions.surfaceRotation
|
|
14
|
+
import com.margelo.nitro.camera.public.NativeCameraOutput
|
|
15
|
+
import com.faceantispoof.FaceAntiSpoofingAdvanced
|
|
16
|
+
import java.util.concurrent.Executors
|
|
17
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
18
|
+
|
|
19
|
+
class HybridSpoofDetectorOutput(
|
|
20
|
+
private val options: SpoofDetectorOutputOptions,
|
|
21
|
+
) : HybridCameraOutputSpec(), ImageAnalysis.Analyzer, NativeCameraOutput {
|
|
22
|
+
override val mediaType: MediaType = MediaType.VIDEO
|
|
23
|
+
override val mirrorMode: MirrorMode = MirrorMode.AUTO
|
|
24
|
+
override var outputOrientation: CameraOrientation = CameraOrientation.UP
|
|
25
|
+
set(value) {
|
|
26
|
+
field = value
|
|
27
|
+
imageAnalysis?.targetRotation = value.surfaceRotation
|
|
28
|
+
}
|
|
29
|
+
override val currentResolution: com.margelo.nitro.camera.Size?
|
|
30
|
+
get() = null
|
|
31
|
+
|
|
32
|
+
private val executor = Executors.newSingleThreadExecutor()
|
|
33
|
+
private val busy = AtomicBoolean(false)
|
|
34
|
+
private val detector = FaceAntiSpoofingAdvanced(
|
|
35
|
+
NitroModules.applicationContext?.assets ?: error("FaceAntiSpoof: no application context")
|
|
36
|
+
)
|
|
37
|
+
private var imageAnalysis: ImageAnalysis? = null
|
|
38
|
+
|
|
39
|
+
override fun createUseCase(
|
|
40
|
+
mirrorMode: MirrorMode,
|
|
41
|
+
config: NativeCameraOutput.Config,
|
|
42
|
+
): NativeCameraOutput.PreparedUseCase {
|
|
43
|
+
val target = options.targetResolution
|
|
44
|
+
val selector = target?.let {
|
|
45
|
+
ResolutionSelector.Builder()
|
|
46
|
+
.setResolutionStrategy(
|
|
47
|
+
ResolutionStrategy(
|
|
48
|
+
android.util.Size(it.width.toInt(), it.height.toInt()),
|
|
49
|
+
ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
.build()
|
|
53
|
+
}
|
|
54
|
+
val analysis = ImageAnalysis.Builder()
|
|
55
|
+
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
|
|
56
|
+
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
57
|
+
.setAllowDroppingLateFrames(true)
|
|
58
|
+
.apply { if (selector != null) setResolutionSelector(selector) }
|
|
59
|
+
.build()
|
|
60
|
+
return NativeCameraOutput.PreparedUseCase(analysis) {
|
|
61
|
+
imageAnalysis = analysis
|
|
62
|
+
analysis.setAnalyzer(executor, this)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
override fun analyze(image: ImageProxy) {
|
|
67
|
+
if (!busy.compareAndSet(false, true)) {
|
|
68
|
+
image.close()
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
val planes = image.planes.take(3).map { plane ->
|
|
73
|
+
val buffer = plane.buffer.duplicate()
|
|
74
|
+
ByteArray(buffer.remaining()).also(buffer::get)
|
|
75
|
+
}.toTypedArray()
|
|
76
|
+
val rowStrides = image.planes.take(3).map { it.rowStride }.toIntArray()
|
|
77
|
+
val pixelStrides = image.planes.take(3).map { it.pixelStride.coerceAtLeast(1) }.toIntArray()
|
|
78
|
+
val result = detector.advancedAntiSpoofing(
|
|
79
|
+
planes, image.width, image.height, rowStrides, pixelStrides
|
|
80
|
+
)
|
|
81
|
+
val isLive = result.neuralNetworkScore < FaceAntiSpoofingAdvanced.BASE_THRESHOLD &&
|
|
82
|
+
result.laplacianScore > FaceAntiSpoofingAdvanced.LAPLACIAN_THRESHOLD
|
|
83
|
+
options.onResult.invoke(
|
|
84
|
+
AntiSpoofingResult(
|
|
85
|
+
isLive,
|
|
86
|
+
if (isLive) "Live Face" else "Spoof Face",
|
|
87
|
+
result.neuralNetworkScore.toDouble(),
|
|
88
|
+
result.laplacianScore.toDouble(),
|
|
89
|
+
result.combinedScore.toDouble(),
|
|
90
|
+
result.confidence.toDouble(),
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
} catch (error: Throwable) {
|
|
94
|
+
options.onError?.invoke(error)
|
|
95
|
+
} finally {
|
|
96
|
+
image.close()
|
|
97
|
+
busy.set(false)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override fun dispose() {
|
|
102
|
+
imageAnalysis?.clearAnalyzer()
|
|
103
|
+
detector.close()
|
|
104
|
+
executor.shutdown()
|
|
105
|
+
super.dispose()
|
|
106
|
+
}
|
|
107
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* TypeScript type definitions
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { Frame, FrameProcessor } from 'react-native-vision-camera';
|
|
7
|
+
import type { CameraFrameOutput, Frame, FrameOutputOptions, FrameProcessor } from 'react-native-vision-camera';
|
|
8
8
|
import type { SharedValue } from 'react-native-reanimated';
|
|
9
9
|
|
|
10
10
|
// ============================================================================
|
|
@@ -281,6 +281,19 @@ export interface UseFaceDetectionFrameProcessorReturn {
|
|
|
281
281
|
antiSpoofState: AntiSpoofState;
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
/**
|
|
285
|
+
* VisionCamera 5 frame-output options for spoof detection.
|
|
286
|
+
* The callback runs on the React Native runtime after native inference.
|
|
287
|
+
*/
|
|
288
|
+
export interface UseFaceAntiSpoofFrameOutputOptions extends Partial<FrameOutputOptions> {
|
|
289
|
+
onResult?: (result: FaceAntiSpoofingResult) => void;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Create a VisionCamera 5 frame output backed by the native detector. */
|
|
293
|
+
export function useFaceAntiSpoofFrameOutput(
|
|
294
|
+
options?: UseFaceAntiSpoofFrameOutputOptions,
|
|
295
|
+
): CameraFrameOutput;
|
|
296
|
+
|
|
284
297
|
// ============================================================================
|
|
285
298
|
// Module Functions
|
|
286
299
|
// ============================================================================
|
package/index.js
CHANGED
|
@@ -1,60 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useMemo, useRef } from 'react';
|
|
2
|
+
import { NativeModules } from 'react-native';
|
|
3
|
+
import { NitroModules } from 'react-native-nitro-modules';
|
|
4
|
+
import { scheduleOnRN } from 'react-native-worklets';
|
|
3
5
|
|
|
4
6
|
const { FaceAntiSpoof } = NativeModules;
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// Worklet called by VisionCamera
|
|
28
|
-
export const faceAntiSpoofFrameProcessor = function (frame) {
|
|
29
|
-
'worklet';
|
|
30
|
-
try {
|
|
31
|
-
if (!_faceAntiSpoofPlugin) {
|
|
32
|
-
console.warn('[FaceAntiSpoof] Plugin not initialized');
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (typeof _faceAntiSpoofPlugin.call !== 'function') {
|
|
37
|
-
console.warn('[FaceAntiSpoof] Plugin call method not available');
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (!frame) {
|
|
42
|
-
console.warn('[FaceAntiSpoof] Frame is null or undefined');
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const result = _faceAntiSpoofPlugin.call(frame);
|
|
47
|
-
|
|
48
|
-
// Validate result structure
|
|
49
|
-
if (result && typeof result === 'object') {
|
|
50
|
-
return result;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return null;
|
|
54
|
-
} catch (err) {
|
|
55
|
-
console.error('[FaceAntiSpoof] Error in frame processor:', err);
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
8
|
+
export const useFaceAntiSpoofFrameOutput = ({ onResult, ...options } = {}) => {
|
|
9
|
+
const resultRef = useRef(onResult);
|
|
10
|
+
resultRef.current = onResult;
|
|
11
|
+
const errorRef = useRef(options.onError);
|
|
12
|
+
errorRef.current = options.onError;
|
|
13
|
+
const factory = useMemo(
|
|
14
|
+
() => NitroModules.createHybridObject('SpoofDetectorFactory'),
|
|
15
|
+
[],
|
|
16
|
+
);
|
|
17
|
+
return useMemo(
|
|
18
|
+
() => factory.createSpoofDetectorOutput({
|
|
19
|
+
targetResolution: options.targetResolution ?? { width: 256, height: 256 },
|
|
20
|
+
onResult(result) {
|
|
21
|
+
resultRef.current?.(result);
|
|
22
|
+
},
|
|
23
|
+
onError(error) {
|
|
24
|
+
errorRef.current?.(error);
|
|
25
|
+
},
|
|
26
|
+
}),
|
|
27
|
+
[factory, options.targetResolution],
|
|
28
|
+
);
|
|
58
29
|
};
|
|
59
30
|
|
|
60
31
|
export default FaceAntiSpoof;
|
|
@@ -86,9 +57,7 @@ export const initializeFaceAntiSpoof = async () => {
|
|
|
86
57
|
}
|
|
87
58
|
|
|
88
59
|
const isSuccess = result && status.pluginAvailable;
|
|
89
|
-
|
|
90
|
-
const pluginInitialized = isSuccess ? ensurePluginInitialized() : false;
|
|
91
|
-
const initialized = isSuccess && pluginInitialized;
|
|
60
|
+
const initialized = isSuccess;
|
|
92
61
|
console.log('[FaceAntiSpoof] Initialization result:', initialized);
|
|
93
62
|
return initialized;
|
|
94
63
|
} catch (error) {
|
package/nitro.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://nitro.margelo.com/nitro.schema.json",
|
|
3
|
+
"cxxNamespace": ["camera", "spoofdetector"],
|
|
4
|
+
"ios": {
|
|
5
|
+
"iosModuleName": "VisionCameraSpoofDetector"
|
|
6
|
+
},
|
|
7
|
+
"android": {
|
|
8
|
+
"androidNamespace": ["camera", "spoofdetector"],
|
|
9
|
+
"androidCxxLibName": "VisionCameraSpoofDetector"
|
|
10
|
+
},
|
|
11
|
+
"autolinking": {
|
|
12
|
+
"SpoofDetectorFactory": {
|
|
13
|
+
"android": {
|
|
14
|
+
"language": "kotlin",
|
|
15
|
+
"implementationClassName": "HybridSpoofDetectorFactory"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"ignorePaths": ["**/node_modules"]
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
** linguist-generated=true
|