react-native-vision-camera-spoof-detector 1.1.15 → 1.1.17
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,51 @@ 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 sampled = sampleRgbaToModelAndGray(rgba, width, height)
|
|
202
|
+
val nnResult = neuralNetworkAntiSpoofingSampled(sampled.rgbBuffer)
|
|
203
|
+
val nnScore = nnResult.getOrNull() ?: throw IllegalStateException(nnResult.exceptionOrNull()?.message ?: "NN inference failed")
|
|
204
|
+
val laplacianScore = calculateLaplacianFromGray(sampled.grayBuffer)
|
|
205
|
+
val isLive = nnScore < BASE_THRESHOLD && laplacianScore > LAPLACIAN_THRESHOLD
|
|
206
|
+
val combinedScore = calculateCombinedScore(nnScore, laplacianScore)
|
|
207
|
+
val confidence = calculateConfidence(nnScore, laplacianScore)
|
|
208
|
+
|
|
209
|
+
AntiSpoofingResult(
|
|
210
|
+
neuralNetworkScore = nnScore,
|
|
211
|
+
laplacianScore = laplacianScore,
|
|
212
|
+
textureComplexity = 0,
|
|
213
|
+
temporalConsistency = 0f,
|
|
214
|
+
colorSaturation = 0f,
|
|
215
|
+
combinedScore = combinedScore,
|
|
216
|
+
confidence = confidence
|
|
217
|
+
)
|
|
218
|
+
} catch (e: Exception) {
|
|
219
|
+
Log.e(TAG, "Error in advancedAntiSpoofingRGBA", e)
|
|
220
|
+
AntiSpoofingResult(
|
|
221
|
+
neuralNetworkScore = 0f,
|
|
222
|
+
laplacianScore = 0,
|
|
223
|
+
textureComplexity = 0,
|
|
224
|
+
temporalConsistency = 0f,
|
|
225
|
+
colorSaturation = 0f,
|
|
226
|
+
combinedScore = 0f,
|
|
227
|
+
confidence = 0f
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
187
232
|
// FROM WORKING VERSION: neuralNetworkAntiSpoofing with Bitmap
|
|
188
233
|
private fun neuralNetworkAntiSpoofing(bitmap: Bitmap): Result<Float> {
|
|
189
234
|
return try {
|
|
@@ -216,6 +261,46 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
216
261
|
}
|
|
217
262
|
}
|
|
218
263
|
|
|
264
|
+
private fun neuralNetworkAntiSpoofingRGBA(rgba: ByteArray, width: Int, height: Int): Result<Float> {
|
|
265
|
+
return try {
|
|
266
|
+
val img = normalizeRgbaImage(rgba, width, height, INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE)
|
|
267
|
+
val input = arrayOf(img)
|
|
268
|
+
|
|
269
|
+
clssPred[0].fill(0f)
|
|
270
|
+
leafNodeMask[0].fill(0f)
|
|
271
|
+
|
|
272
|
+
outputs[outputIndex0] = clssPred
|
|
273
|
+
outputs[outputIndex1] = leafNodeMask
|
|
274
|
+
|
|
275
|
+
interpreter!!.runForMultipleInputsOutputs(arrayOf(input), outputs)
|
|
276
|
+
|
|
277
|
+
Log.i(TAG, "clss_pred: [${clssPred[0].joinToString()}]")
|
|
278
|
+
Log.i(TAG, "leaf_node_mask: [${leafNodeMask[0].joinToString()}]")
|
|
279
|
+
|
|
280
|
+
Result.success(leafScore1(clssPred, leafNodeMask))
|
|
281
|
+
} catch (e: Exception) {
|
|
282
|
+
Log.e(TAG, "Error in neuralNetworkAntiSpoofingRGBA", e)
|
|
283
|
+
Result.failure(e)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private fun neuralNetworkAntiSpoofingSampled(rgbBuffer: Array<Array<FloatArray>>): Result<Float> {
|
|
288
|
+
return try {
|
|
289
|
+
val input = arrayOf(rgbBuffer)
|
|
290
|
+
clssPred[0].fill(0f)
|
|
291
|
+
leafNodeMask[0].fill(0f)
|
|
292
|
+
outputs[outputIndex0] = clssPred
|
|
293
|
+
outputs[outputIndex1] = leafNodeMask
|
|
294
|
+
interpreter!!.runForMultipleInputsOutputs(arrayOf(input), outputs)
|
|
295
|
+
Log.i(TAG, "clss_pred: [${clssPred[0].joinToString()}]")
|
|
296
|
+
Log.i(TAG, "leaf_node_mask: [${leafNodeMask[0].joinToString()}]")
|
|
297
|
+
Result.success(leafScore1(clssPred, leafNodeMask))
|
|
298
|
+
} catch (e: Exception) {
|
|
299
|
+
Log.e(TAG, "Error in neuralNetworkAntiSpoofingSampled", e)
|
|
300
|
+
Result.failure(e)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
219
304
|
// FROM WORKING VERSION: leafScore1
|
|
220
305
|
private fun leafScore1(clssPred: Array<FloatArray>, leafNodeMask: Array<FloatArray>): Float {
|
|
221
306
|
var score = 0f
|
|
@@ -247,6 +332,67 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
247
332
|
return floatValues
|
|
248
333
|
}
|
|
249
334
|
|
|
335
|
+
private fun normalizeRgbaImage(
|
|
336
|
+
rgba: ByteArray,
|
|
337
|
+
srcWidth: Int,
|
|
338
|
+
srcHeight: Int,
|
|
339
|
+
targetWidth: Int,
|
|
340
|
+
targetHeight: Int
|
|
341
|
+
): Array<Array<FloatArray>> {
|
|
342
|
+
val floatValues = Array(targetHeight) { Array(targetWidth) { FloatArray(3) } }
|
|
343
|
+
val scaleX = srcWidth.toFloat() / targetWidth
|
|
344
|
+
val scaleY = srcHeight.toFloat() / targetHeight
|
|
345
|
+
|
|
346
|
+
for (targetY in 0 until targetHeight) {
|
|
347
|
+
val srcY = (targetY * scaleY).toInt().coerceIn(0, srcHeight - 1)
|
|
348
|
+
for (targetX in 0 until targetWidth) {
|
|
349
|
+
val srcX = (targetX * scaleX).toInt().coerceIn(0, srcWidth - 1)
|
|
350
|
+
val srcIndex = (srcY * srcWidth + srcX) * 4
|
|
351
|
+
|
|
352
|
+
val r = (rgba[srcIndex].toInt() and 0xFF) / 255f
|
|
353
|
+
val g = (rgba[srcIndex + 1].toInt() and 0xFF) / 255f
|
|
354
|
+
val b = (rgba[srcIndex + 2].toInt() and 0xFF) / 255f
|
|
355
|
+
floatValues[targetY][targetX] = floatArrayOf(r, g, b)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return floatValues
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private data class SampledFrame(
|
|
363
|
+
val rgbBuffer: Array<Array<FloatArray>>,
|
|
364
|
+
val grayBuffer: Array<IntArray>
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
private fun sampleRgbaToModelAndGray(
|
|
368
|
+
rgba: ByteArray,
|
|
369
|
+
srcWidth: Int,
|
|
370
|
+
srcHeight: Int
|
|
371
|
+
): SampledFrame {
|
|
372
|
+
val targetWidth = INPUT_IMAGE_SIZE
|
|
373
|
+
val targetHeight = INPUT_IMAGE_SIZE
|
|
374
|
+
val rgbBuffer = Array(targetHeight) { Array(targetWidth) { FloatArray(3) } }
|
|
375
|
+
val grayBuffer = Array(targetHeight) { IntArray(targetWidth) }
|
|
376
|
+
val scaleX = srcWidth.toFloat() / targetWidth
|
|
377
|
+
val scaleY = srcHeight.toFloat() / targetHeight
|
|
378
|
+
|
|
379
|
+
for (targetY in 0 until targetHeight) {
|
|
380
|
+
val srcY = (targetY * scaleY).toInt().coerceIn(0, srcHeight - 1)
|
|
381
|
+
for (targetX in 0 until targetWidth) {
|
|
382
|
+
val srcX = (targetX * scaleX).toInt().coerceIn(0, srcWidth - 1)
|
|
383
|
+
val srcIndex = (srcY * srcWidth + srcX) * 4
|
|
384
|
+
|
|
385
|
+
val r = (rgba[srcIndex].toInt() and 0xFF) / 255f
|
|
386
|
+
val g = (rgba[srcIndex + 1].toInt() and 0xFF) / 255f
|
|
387
|
+
val b = (rgba[srcIndex + 2].toInt() and 0xFF) / 255f
|
|
388
|
+
rgbBuffer[targetY][targetX] = floatArrayOf(r, g, b)
|
|
389
|
+
grayBuffer[targetY][targetX] = (0.299f * rgba[srcIndex].toInt() + 0.587f * rgba[srcIndex + 1].toInt() + 0.114f * rgba[srcIndex + 2].toInt()).toInt()
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return SampledFrame(rgbBuffer, grayBuffer)
|
|
394
|
+
}
|
|
395
|
+
|
|
250
396
|
// FROM WORKING VERSION: laplacian with Bitmap
|
|
251
397
|
private fun laplacian(bitmap: Bitmap): Result<Int> {
|
|
252
398
|
return try {
|
|
@@ -264,6 +410,17 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
264
410
|
}
|
|
265
411
|
}
|
|
266
412
|
|
|
413
|
+
private fun laplacianFromRgba(rgba: ByteArray, width: Int, height: Int): Result<Int> {
|
|
414
|
+
return try {
|
|
415
|
+
val gray = rgbaToGrayscale(rgba, width, height, INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE)
|
|
416
|
+
val score = calculateLaplacianFromGray(gray)
|
|
417
|
+
Result.success(score)
|
|
418
|
+
} catch (e: Exception) {
|
|
419
|
+
Log.e(TAG, "Error in laplacianFromRgba", e)
|
|
420
|
+
Result.failure(e)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
267
424
|
// FROM WORKING VERSION: calculateLaplacianScore
|
|
268
425
|
private fun calculateLaplacianScore(bitmap: Bitmap): Int {
|
|
269
426
|
val laplace = arrayOf(
|
|
@@ -315,6 +472,58 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
315
472
|
return greyImg
|
|
316
473
|
}
|
|
317
474
|
|
|
475
|
+
private fun rgbaToGrayscale(
|
|
476
|
+
rgba: ByteArray,
|
|
477
|
+
srcWidth: Int,
|
|
478
|
+
srcHeight: Int,
|
|
479
|
+
targetWidth: Int,
|
|
480
|
+
targetHeight: Int
|
|
481
|
+
): Array<IntArray> {
|
|
482
|
+
val gray = Array(targetHeight) { IntArray(targetWidth) }
|
|
483
|
+
val scaleX = srcWidth.toFloat() / targetWidth
|
|
484
|
+
val scaleY = srcHeight.toFloat() / targetHeight
|
|
485
|
+
|
|
486
|
+
for (targetY in 0 until targetHeight) {
|
|
487
|
+
val srcY = (targetY * scaleY).toInt().coerceIn(0, srcHeight - 1)
|
|
488
|
+
for (targetX in 0 until targetWidth) {
|
|
489
|
+
val srcX = (targetX * scaleX).toInt().coerceIn(0, srcWidth - 1)
|
|
490
|
+
val srcIndex = (srcY * srcWidth + srcX) * 4
|
|
491
|
+
val r = rgba[srcIndex].toInt() and 0xFF
|
|
492
|
+
val g = rgba[srcIndex + 1].toInt() and 0xFF
|
|
493
|
+
val b = rgba[srcIndex + 2].toInt() and 0xFF
|
|
494
|
+
gray[targetY][targetX] = (0.299 * r + 0.587 * g + 0.114 * b).toInt()
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return gray
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private fun calculateLaplacianFromGray(img: Array<IntArray>): Int {
|
|
502
|
+
val laplace = arrayOf(
|
|
503
|
+
intArrayOf(0, 1, 0),
|
|
504
|
+
intArrayOf(1, -4, 1),
|
|
505
|
+
intArrayOf(0, 1, 0)
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
val height = img.size
|
|
509
|
+
val width = img[0].size
|
|
510
|
+
var score = 0
|
|
511
|
+
for (x in 0 until height - laplace.size + 1) {
|
|
512
|
+
for (y in 0 until width - laplace.size + 1) {
|
|
513
|
+
var result = 0
|
|
514
|
+
for (i in 0 until laplace.size) {
|
|
515
|
+
for (j in 0 until laplace.size) {
|
|
516
|
+
result += img[x + i][y + j] * laplace[i][j]
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (result > LAPLACE_THRESHOLD) {
|
|
520
|
+
score++
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return score
|
|
525
|
+
}
|
|
526
|
+
|
|
318
527
|
// FROM WORKING VERSION: calculateCombinedScore
|
|
319
528
|
private fun calculateCombinedScore(neuralScore: Float, laplacianScore: Int): Float {
|
|
320
529
|
// Normalize both scores to 0-1 range
|
|
@@ -422,6 +631,22 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
422
631
|
return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888)
|
|
423
632
|
}
|
|
424
633
|
|
|
634
|
+
private fun rgbaToBitmap(rgba: ByteArray, width: Int, height: Int): Bitmap {
|
|
635
|
+
val pixels = IntArray(width * height)
|
|
636
|
+
var index = 0
|
|
637
|
+
for (y in 0 until height) {
|
|
638
|
+
for (x in 0 until width) {
|
|
639
|
+
val r = rgba[index].toInt() and 0xFF
|
|
640
|
+
val g = rgba[index + 1].toInt() and 0xFF
|
|
641
|
+
val b = rgba[index + 2].toInt() and 0xFF
|
|
642
|
+
val a = rgba[index + 3].toInt() and 0xFF
|
|
643
|
+
pixels[y * width + x] = (a shl 24) or (r shl 16) or (g shl 8) or b
|
|
644
|
+
index += 4
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888)
|
|
648
|
+
}
|
|
649
|
+
|
|
425
650
|
@Throws(IOException::class)
|
|
426
651
|
private fun loadModelFile(assetManager: AssetManager, modelName: String): MappedByteBuffer {
|
|
427
652
|
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.17",
|
|
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": {
|