react-native-vision-camera-spoof-detector 1.1.15 → 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.
|
@@ -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()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-vision-camera-spoof-detector",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.16",
|
|
4
4
|
"description": "High-performance face anti-spoofing and liveness detection module for React Native Vision Camera. Uses TensorFlow Lite with GPU acceleration and optimized YUV processing.",
|
|
5
5
|
"homepage": "https://github.com/dpraful/react-native-vision-camera-spoof-detector",
|
|
6
6
|
"repository": {
|