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 {
@@ -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.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": {