react-native-vision-camera-spoof-detector 1.1.20 → 1.1.22

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.
@@ -292,29 +292,6 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
292
292
  }
293
293
  }
294
294
 
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
295
  private fun neuralNetworkAntiSpoofingSampled(rgbBuffer: Array<Array<FloatArray>>): Result<Float> {
319
296
  return try {
320
297
  val input = arrayOf(rgbBuffer)
@@ -363,38 +340,13 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
363
340
  return floatValues
364
341
  }
365
342
 
366
- private fun normalizeRgbaImage(
367
- rgba: ByteArray,
368
- srcWidth: Int,
369
- srcHeight: Int,
370
- targetWidth: Int,
371
- targetHeight: Int
372
- ): Array<Array<FloatArray>> {
373
- val floatValues = Array(targetHeight) { Array(targetWidth) { FloatArray(3) } }
374
- val scaleX = srcWidth.toFloat() / targetWidth
375
- val scaleY = srcHeight.toFloat() / targetHeight
376
-
377
- for (targetY in 0 until targetHeight) {
378
- val srcY = (targetY * scaleY).toInt().coerceIn(0, srcHeight - 1)
379
- for (targetX in 0 until targetWidth) {
380
- val srcX = (targetX * scaleX).toInt().coerceIn(0, srcWidth - 1)
381
- val srcIndex = (srcY * srcWidth + srcX) * 4
382
-
383
- val r = (rgba[srcIndex].toInt() and 0xFF) / 255f
384
- val g = (rgba[srcIndex + 1].toInt() and 0xFF) / 255f
385
- val b = (rgba[srcIndex + 2].toInt() and 0xFF) / 255f
386
- floatValues[targetY][targetX] = floatArrayOf(r, g, b)
387
- }
388
- }
389
-
390
- return floatValues
391
- }
392
-
393
343
  private data class SampledFrame(
394
344
  val rgbBuffer: Array<Array<FloatArray>>,
395
345
  val grayBuffer: Array<IntArray>
396
346
  )
397
347
 
348
+ // Single-pass resize: one source pixel sample writes both the model RGB tensor
349
+ // and the grayscale buffer used for Laplacian scoring.
398
350
  private fun sampleRgbaToModelAndGray(
399
351
  rgba: ByteArray,
400
352
  srcWidth: Int,
@@ -446,17 +398,6 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
446
398
  }
447
399
  }
448
400
 
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
401
  // FROM WORKING VERSION: calculateLaplacianScore
461
402
  private fun calculateLaplacianScore(bitmap: Bitmap): Int {
462
403
  val laplace = arrayOf(
@@ -508,32 +449,6 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
508
449
  return greyImg
509
450
  }
510
451
 
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
452
  private fun calculateLaplacianFromGray(img: Array<IntArray>): Int {
538
453
  val laplace = arrayOf(
539
454
  intArrayOf(0, 1, 0),
@@ -13,6 +13,9 @@ import com.margelo.nitro.camera.extensions.setAllowDroppingLateFrames
13
13
  import com.margelo.nitro.camera.extensions.surfaceRotation
14
14
  import com.margelo.nitro.camera.public.NativeCameraOutput
15
15
  import com.faceantispoof.FaceAntiSpoofingAdvanced
16
+ import kotlin.math.abs
17
+ import kotlin.math.max
18
+ import kotlin.math.min
16
19
  import java.util.concurrent.Executors
17
20
  import java.util.concurrent.atomic.AtomicBoolean
18
21
 
@@ -32,10 +35,19 @@ class HybridSpoofDetectorOutput(
32
35
  private val executor = Executors.newSingleThreadExecutor()
33
36
  private val busy = AtomicBoolean(false)
34
37
  private val disposed = AtomicBoolean(false)
38
+ private val frameCounter = java.util.concurrent.atomic.AtomicInteger(0)
39
+ private var lastFrameBrightness = 0f
35
40
  private val detector = FaceAntiSpoofingAdvanced(
36
41
  NitroModules.applicationContext?.assets ?: error("FaceAntiSpoof: no application context")
37
42
  )
38
43
  private var imageAnalysis: ImageAnalysis? = null
44
+ private companion object {
45
+ private const val FRAME_EVALUATION_INTERVAL = 3
46
+ private const val MIN_FRAME_BRIGHTNESS = 24f
47
+ private const val MAX_FRAME_BRIGHTNESS = 220f
48
+ private const val MIN_VARIANCE_FOR_ANALYSIS = 1500f
49
+ private const val MIN_BRIGHTNESS_DELTA = 8f
50
+ }
39
51
 
40
52
  override fun createUseCase(
41
53
  mirrorMode: MirrorMode,
@@ -72,6 +84,10 @@ class HybridSpoofDetectorOutput(
72
84
  }
73
85
  try {
74
86
  val rgba = extractRgbaBytes(image)
87
+ if (!shouldProcessFrame(rgba, image.width, image.height)) {
88
+ return
89
+ }
90
+
75
91
  val result = detector.advancedAntiSpoofingRGBA(rgba, image.width, image.height)
76
92
  val isLive = result.neuralNetworkScore < FaceAntiSpoofingAdvanced.BASE_THRESHOLD &&
77
93
  result.laplacianScore > FaceAntiSpoofingAdvanced.LAPLACIAN_THRESHOLD
@@ -93,6 +109,50 @@ class HybridSpoofDetectorOutput(
93
109
  }
94
110
  }
95
111
 
112
+ private fun shouldProcessFrame(rgba: ByteArray, width: Int, height: Int): Boolean {
113
+ val frameIndex = frameCounter.incrementAndGet()
114
+ if (frameIndex % FRAME_EVALUATION_INTERVAL != 0) {
115
+ return false
116
+ }
117
+
118
+ val sampleStep = max(1, min(width, height) / 32)
119
+ var brightnessSum = 0L
120
+ var brightnessSquaredSum = 0L
121
+ var sampleCount = 0
122
+
123
+ for (y in 0 until height step sampleStep) {
124
+ val rowOffset = y * width * 4
125
+ for (x in 0 until width step sampleStep) {
126
+ val index = rowOffset + x * 4
127
+ if (index + 2 >= rgba.size) continue
128
+
129
+ val r = rgba[index].toInt() and 0xFF
130
+ val g = rgba[index + 1].toInt() and 0xFF
131
+ val b = rgba[index + 2].toInt() and 0xFF
132
+ val luma = (0.299 * r + 0.587 * g + 0.114 * b).toInt()
133
+
134
+ brightnessSum += luma.toLong()
135
+ brightnessSquaredSum += (luma.toLong() * luma.toLong())
136
+ sampleCount += 1
137
+ }
138
+ }
139
+
140
+ if (sampleCount == 0) {
141
+ return false
142
+ }
143
+
144
+ val avgBrightness = brightnessSum.toFloat() / sampleCount
145
+ val variance = (brightnessSquaredSum.toFloat() / sampleCount) - (avgBrightness * avgBrightness)
146
+ val brightnessDelta = kotlin.math.abs(avgBrightness - lastFrameBrightness)
147
+ lastFrameBrightness = avgBrightness
148
+
149
+ val brightnessOk = avgBrightness in MIN_FRAME_BRIGHTNESS..MAX_FRAME_BRIGHTNESS
150
+ val textureOk = variance >= MIN_VARIANCE_FOR_ANALYSIS
151
+ val motionLikely = brightnessDelta >= MIN_BRIGHTNESS_DELTA || lastFrameBrightness == 0f
152
+
153
+ return brightnessOk && textureOk && motionLikely
154
+ }
155
+
96
156
  private fun extractRgbaBytes(image: ImageProxy): ByteArray {
97
157
  val plane = image.planes.firstOrNull() ?: throw IllegalStateException("RGBA image has no planes")
98
158
  val rowStride = plane.rowStride
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-vision-camera-spoof-detector",
3
- "version": "1.1.20",
3
+ "version": "1.1.22",
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": {