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 {
@@ -53,7 +53,7 @@ class HybridSpoofDetectorOutput(
53
53
  .build()
54
54
  }
55
55
  val analysis = ImageAnalysis.Builder()
56
- .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
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 sourcePlanes = image.planes
75
- val planes = Array(3) { index ->
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.15",
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": {