react-native-vision-camera-spoof-detector 1.1.21 → 1.1.23
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.
|
@@ -260,6 +260,50 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
fun advancedAntiSpoofingRGBA(buffer: ByteBuffer, width: Int, height: Int, rowStride: Int, pixelStride: Int): AntiSpoofingResult {
|
|
264
|
+
if (!isInitialized) {
|
|
265
|
+
return AntiSpoofingResult(
|
|
266
|
+
neuralNetworkScore = 0f,
|
|
267
|
+
laplacianScore = 0,
|
|
268
|
+
textureComplexity = 0,
|
|
269
|
+
temporalConsistency = 0f,
|
|
270
|
+
colorSaturation = 0f,
|
|
271
|
+
combinedScore = 0f,
|
|
272
|
+
confidence = 0f
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return try {
|
|
277
|
+
val sampled = sampleRgbaBufferToModelAndGray(buffer, width, height, rowStride, pixelStride)
|
|
278
|
+
val nnResult = neuralNetworkAntiSpoofingSampled(sampled.rgbBuffer)
|
|
279
|
+
val nnScore = nnResult.getOrNull() ?: throw IllegalStateException(nnResult.exceptionOrNull()?.message ?: "NN inference failed")
|
|
280
|
+
val laplacianScore = calculateLaplacianFromGray(sampled.grayBuffer)
|
|
281
|
+
val combinedScore = calculateCombinedScore(nnScore, laplacianScore)
|
|
282
|
+
val confidence = calculateConfidence(nnScore, laplacianScore)
|
|
283
|
+
|
|
284
|
+
AntiSpoofingResult(
|
|
285
|
+
neuralNetworkScore = nnScore,
|
|
286
|
+
laplacianScore = laplacianScore,
|
|
287
|
+
textureComplexity = 0,
|
|
288
|
+
temporalConsistency = 0f,
|
|
289
|
+
colorSaturation = 0f,
|
|
290
|
+
combinedScore = combinedScore,
|
|
291
|
+
confidence = confidence
|
|
292
|
+
)
|
|
293
|
+
} catch (e: Exception) {
|
|
294
|
+
Log.e(TAG, "Error in advancedAntiSpoofingRGBA buffer path", e)
|
|
295
|
+
AntiSpoofingResult(
|
|
296
|
+
neuralNetworkScore = 0f,
|
|
297
|
+
laplacianScore = 0,
|
|
298
|
+
textureComplexity = 0,
|
|
299
|
+
temporalConsistency = 0f,
|
|
300
|
+
colorSaturation = 0f,
|
|
301
|
+
combinedScore = 0f,
|
|
302
|
+
confidence = 0f
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
263
307
|
// FROM WORKING VERSION: neuralNetworkAntiSpoofing with Bitmap
|
|
264
308
|
private fun neuralNetworkAntiSpoofing(bitmap: Bitmap): Result<Float> {
|
|
265
309
|
return try {
|
|
@@ -292,29 +336,6 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
292
336
|
}
|
|
293
337
|
}
|
|
294
338
|
|
|
295
|
-
private fun neuralNetworkAntiSpoofingRGBA(rgba: ByteArray, width: Int, height: Int): Result<Float> {
|
|
296
|
-
return try {
|
|
297
|
-
val img = normalizeRgbaImage(rgba, width, height, INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE)
|
|
298
|
-
val input = arrayOf(img)
|
|
299
|
-
|
|
300
|
-
clssPred[0].fill(0f)
|
|
301
|
-
leafNodeMask[0].fill(0f)
|
|
302
|
-
|
|
303
|
-
outputs[outputIndex0] = clssPred
|
|
304
|
-
outputs[outputIndex1] = leafNodeMask
|
|
305
|
-
|
|
306
|
-
interpreter!!.runForMultipleInputsOutputs(arrayOf(input), outputs)
|
|
307
|
-
|
|
308
|
-
Log.i(TAG, "clss_pred: [${clssPred[0].joinToString()}]")
|
|
309
|
-
Log.i(TAG, "leaf_node_mask: [${leafNodeMask[0].joinToString()}]")
|
|
310
|
-
|
|
311
|
-
Result.success(leafScore1(clssPred, leafNodeMask))
|
|
312
|
-
} catch (e: Exception) {
|
|
313
|
-
Log.e(TAG, "Error in neuralNetworkAntiSpoofingRGBA", e)
|
|
314
|
-
Result.failure(e)
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
339
|
private fun neuralNetworkAntiSpoofingSampled(rgbBuffer: Array<Array<FloatArray>>): Result<Float> {
|
|
319
340
|
return try {
|
|
320
341
|
val input = arrayOf(rgbBuffer)
|
|
@@ -363,14 +384,20 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
363
384
|
return floatValues
|
|
364
385
|
}
|
|
365
386
|
|
|
366
|
-
private
|
|
387
|
+
private data class SampledFrame(
|
|
388
|
+
val rgbBuffer: Array<Array<FloatArray>>,
|
|
389
|
+
val grayBuffer: Array<IntArray>
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
// Single-pass resize: one source pixel sample writes both the model RGB tensor
|
|
393
|
+
// and the grayscale buffer used for Laplacian scoring.
|
|
394
|
+
private fun sampleRgbaToModelAndGray(
|
|
367
395
|
rgba: ByteArray,
|
|
368
396
|
srcWidth: Int,
|
|
369
|
-
srcHeight: Int
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
val floatValues = Array(targetHeight) { Array(targetWidth) { FloatArray(3) } }
|
|
397
|
+
srcHeight: Int
|
|
398
|
+
): SampledFrame {
|
|
399
|
+
val targetWidth = INPUT_IMAGE_SIZE
|
|
400
|
+
val targetHeight = INPUT_IMAGE_SIZE
|
|
374
401
|
val scaleX = srcWidth.toFloat() / targetWidth
|
|
375
402
|
val scaleY = srcHeight.toFloat() / targetHeight
|
|
376
403
|
|
|
@@ -383,45 +410,53 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
383
410
|
val r = (rgba[srcIndex].toInt() and 0xFF) / 255f
|
|
384
411
|
val g = (rgba[srcIndex + 1].toInt() and 0xFF) / 255f
|
|
385
412
|
val b = (rgba[srcIndex + 2].toInt() and 0xFF) / 255f
|
|
386
|
-
|
|
413
|
+
|
|
414
|
+
modelInputBuffer[targetY][targetX][0] = r
|
|
415
|
+
modelInputBuffer[targetY][targetX][1] = g
|
|
416
|
+
modelInputBuffer[targetY][targetX][2] = b
|
|
417
|
+
grayBuffer[targetY][targetX] = (
|
|
418
|
+
0.299f * rgba[srcIndex].toInt() +
|
|
419
|
+
0.587f * rgba[srcIndex + 1].toInt() +
|
|
420
|
+
0.114f * rgba[srcIndex + 2].toInt()
|
|
421
|
+
).toInt()
|
|
387
422
|
}
|
|
388
423
|
}
|
|
389
424
|
|
|
390
|
-
return
|
|
425
|
+
return SampledFrame(modelInputBuffer, grayBuffer)
|
|
391
426
|
}
|
|
392
427
|
|
|
393
|
-
private
|
|
394
|
-
|
|
395
|
-
val grayBuffer: Array<IntArray>
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
private fun sampleRgbaToModelAndGray(
|
|
399
|
-
rgba: ByteArray,
|
|
428
|
+
private fun sampleRgbaBufferToModelAndGray(
|
|
429
|
+
buffer: ByteBuffer,
|
|
400
430
|
srcWidth: Int,
|
|
401
|
-
srcHeight: Int
|
|
431
|
+
srcHeight: Int,
|
|
432
|
+
rowStride: Int,
|
|
433
|
+
pixelStride: Int
|
|
402
434
|
): SampledFrame {
|
|
403
435
|
val targetWidth = INPUT_IMAGE_SIZE
|
|
404
436
|
val targetHeight = INPUT_IMAGE_SIZE
|
|
405
437
|
val scaleX = srcWidth.toFloat() / targetWidth
|
|
406
438
|
val scaleY = srcHeight.toFloat() / targetHeight
|
|
439
|
+
val duplicated = buffer.duplicate()
|
|
440
|
+
duplicated.order(ByteOrder.nativeOrder())
|
|
407
441
|
|
|
408
442
|
for (targetY in 0 until targetHeight) {
|
|
409
443
|
val srcY = (targetY * scaleY).toInt().coerceIn(0, srcHeight - 1)
|
|
444
|
+
val rowOffset = srcY * rowStride
|
|
410
445
|
for (targetX in 0 until targetWidth) {
|
|
411
446
|
val srcX = (targetX * scaleX).toInt().coerceIn(0, srcWidth - 1)
|
|
412
|
-
val
|
|
447
|
+
val pixelIndex = rowOffset + srcX * pixelStride
|
|
413
448
|
|
|
414
|
-
val r = (
|
|
415
|
-
val g = (
|
|
416
|
-
val b = (
|
|
449
|
+
val r = duplicated.get(pixelIndex).toInt() and 0xFF
|
|
450
|
+
val g = duplicated.get(pixelIndex + 1).toInt() and 0xFF
|
|
451
|
+
val b = duplicated.get(pixelIndex + 2).toInt() and 0xFF
|
|
417
452
|
|
|
418
|
-
modelInputBuffer[targetY][targetX][0] = r
|
|
419
|
-
modelInputBuffer[targetY][targetX][1] = g
|
|
420
|
-
modelInputBuffer[targetY][targetX][2] = b
|
|
453
|
+
modelInputBuffer[targetY][targetX][0] = r / 255f
|
|
454
|
+
modelInputBuffer[targetY][targetX][1] = g / 255f
|
|
455
|
+
modelInputBuffer[targetY][targetX][2] = b / 255f
|
|
421
456
|
grayBuffer[targetY][targetX] = (
|
|
422
|
-
0.299f *
|
|
423
|
-
0.587f *
|
|
424
|
-
0.114f *
|
|
457
|
+
0.299f * r +
|
|
458
|
+
0.587f * g +
|
|
459
|
+
0.114f * b
|
|
425
460
|
).toInt()
|
|
426
461
|
}
|
|
427
462
|
}
|
|
@@ -446,17 +481,6 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
446
481
|
}
|
|
447
482
|
}
|
|
448
483
|
|
|
449
|
-
private fun laplacianFromRgba(rgba: ByteArray, width: Int, height: Int): Result<Int> {
|
|
450
|
-
return try {
|
|
451
|
-
val gray = rgbaToGrayscale(rgba, width, height, INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE)
|
|
452
|
-
val score = calculateLaplacianFromGray(gray)
|
|
453
|
-
Result.success(score)
|
|
454
|
-
} catch (e: Exception) {
|
|
455
|
-
Log.e(TAG, "Error in laplacianFromRgba", e)
|
|
456
|
-
Result.failure(e)
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
484
|
// FROM WORKING VERSION: calculateLaplacianScore
|
|
461
485
|
private fun calculateLaplacianScore(bitmap: Bitmap): Int {
|
|
462
486
|
val laplace = arrayOf(
|
|
@@ -508,32 +532,6 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
|
|
|
508
532
|
return greyImg
|
|
509
533
|
}
|
|
510
534
|
|
|
511
|
-
private fun rgbaToGrayscale(
|
|
512
|
-
rgba: ByteArray,
|
|
513
|
-
srcWidth: Int,
|
|
514
|
-
srcHeight: Int,
|
|
515
|
-
targetWidth: Int,
|
|
516
|
-
targetHeight: Int
|
|
517
|
-
): Array<IntArray> {
|
|
518
|
-
val gray = Array(targetHeight) { IntArray(targetWidth) }
|
|
519
|
-
val scaleX = srcWidth.toFloat() / targetWidth
|
|
520
|
-
val scaleY = srcHeight.toFloat() / targetHeight
|
|
521
|
-
|
|
522
|
-
for (targetY in 0 until targetHeight) {
|
|
523
|
-
val srcY = (targetY * scaleY).toInt().coerceIn(0, srcHeight - 1)
|
|
524
|
-
for (targetX in 0 until targetWidth) {
|
|
525
|
-
val srcX = (targetX * scaleX).toInt().coerceIn(0, srcWidth - 1)
|
|
526
|
-
val srcIndex = (srcY * srcWidth + srcX) * 4
|
|
527
|
-
val r = rgba[srcIndex].toInt() and 0xFF
|
|
528
|
-
val g = rgba[srcIndex + 1].toInt() and 0xFF
|
|
529
|
-
val b = rgba[srcIndex + 2].toInt() and 0xFF
|
|
530
|
-
gray[targetY][targetX] = (0.299 * r + 0.587 * g + 0.114 * b).toInt()
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
return gray
|
|
535
|
-
}
|
|
536
|
-
|
|
537
535
|
private fun calculateLaplacianFromGray(img: Array<IntArray>): Int {
|
|
538
536
|
val laplace = arrayOf(
|
|
539
537
|
intArrayOf(0, 1, 0),
|
package/android/src/main/java/com/margelo/nitro/camera/spoofdetector/HybridSpoofDetectorOutput.kt
CHANGED
|
@@ -83,12 +83,21 @@ class HybridSpoofDetectorOutput(
|
|
|
83
83
|
return
|
|
84
84
|
}
|
|
85
85
|
try {
|
|
86
|
-
val
|
|
87
|
-
|
|
86
|
+
val plane = image.planes.firstOrNull() ?: throw IllegalStateException("RGBA image has no planes")
|
|
87
|
+
val rgba = plane.buffer.duplicate()
|
|
88
|
+
rgba.order(java.nio.ByteOrder.nativeOrder())
|
|
89
|
+
|
|
90
|
+
if (!shouldProcessFrameFromPlane(rgba, image.width, image.height, plane.rowStride, plane.pixelStride)) {
|
|
88
91
|
return
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
val result = detector.advancedAntiSpoofingRGBA(
|
|
94
|
+
val result = detector.advancedAntiSpoofingRGBA(
|
|
95
|
+
rgba,
|
|
96
|
+
image.width,
|
|
97
|
+
image.height,
|
|
98
|
+
plane.rowStride,
|
|
99
|
+
plane.pixelStride,
|
|
100
|
+
)
|
|
92
101
|
val isLive = result.neuralNetworkScore < FaceAntiSpoofingAdvanced.BASE_THRESHOLD &&
|
|
93
102
|
result.laplacianScore > FaceAntiSpoofingAdvanced.LAPLACIAN_THRESHOLD
|
|
94
103
|
options.onResult.invoke(
|
|
@@ -109,7 +118,13 @@ class HybridSpoofDetectorOutput(
|
|
|
109
118
|
}
|
|
110
119
|
}
|
|
111
120
|
|
|
112
|
-
private fun
|
|
121
|
+
private fun shouldProcessFrameFromPlane(
|
|
122
|
+
buffer: java.nio.ByteBuffer,
|
|
123
|
+
width: Int,
|
|
124
|
+
height: Int,
|
|
125
|
+
rowStride: Int,
|
|
126
|
+
pixelStride: Int,
|
|
127
|
+
): Boolean {
|
|
113
128
|
val frameIndex = frameCounter.incrementAndGet()
|
|
114
129
|
if (frameIndex % FRAME_EVALUATION_INTERVAL != 0) {
|
|
115
130
|
return false
|
|
@@ -121,14 +136,14 @@ class HybridSpoofDetectorOutput(
|
|
|
121
136
|
var sampleCount = 0
|
|
122
137
|
|
|
123
138
|
for (y in 0 until height step sampleStep) {
|
|
124
|
-
val rowOffset = y *
|
|
139
|
+
val rowOffset = y * rowStride
|
|
125
140
|
for (x in 0 until width step sampleStep) {
|
|
126
|
-
val index = rowOffset + x *
|
|
127
|
-
if (index + 2 >=
|
|
141
|
+
val index = rowOffset + x * pixelStride
|
|
142
|
+
if (index + 2 >= buffer.capacity()) continue
|
|
128
143
|
|
|
129
|
-
val r =
|
|
130
|
-
val g =
|
|
131
|
-
val b =
|
|
144
|
+
val r = buffer.get(index).toInt() and 0xFF
|
|
145
|
+
val g = buffer.get(index + 1).toInt() and 0xFF
|
|
146
|
+
val b = buffer.get(index + 2).toInt() and 0xFF
|
|
132
147
|
val luma = (0.299 * r + 0.587 * g + 0.114 * b).toInt()
|
|
133
148
|
|
|
134
149
|
brightnessSum += luma.toLong()
|
|
@@ -143,7 +158,7 @@ class HybridSpoofDetectorOutput(
|
|
|
143
158
|
|
|
144
159
|
val avgBrightness = brightnessSum.toFloat() / sampleCount
|
|
145
160
|
val variance = (brightnessSquaredSum.toFloat() / sampleCount) - (avgBrightness * avgBrightness)
|
|
146
|
-
val brightnessDelta =
|
|
161
|
+
val brightnessDelta = abs(avgBrightness - lastFrameBrightness)
|
|
147
162
|
lastFrameBrightness = avgBrightness
|
|
148
163
|
|
|
149
164
|
val brightnessOk = avgBrightness in MIN_FRAME_BRIGHTNESS..MAX_FRAME_BRIGHTNESS
|
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.23",
|
|
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": {
|