@rick427/react-native-liveness 0.1.3 → 0.1.5
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.
- package/android/src/main/java/com/livenesscamera/LivenessCameraPackage.kt +3 -2
- package/android/src/main/java/com/livenesscamera/LivenessCameraPlugin.kt +30 -19
- package/lib/module/LivenessCamera.js +29 -18
- package/lib/module/LivenessCamera.js.map +1 -1
- package/lib/typescript/src/LivenessCamera.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/LivenessCamera.tsx +27 -15
|
@@ -12,8 +12,9 @@ class LivenessCameraPackage : ReactPackage {
|
|
|
12
12
|
init {
|
|
13
13
|
// Register the frame processor plugin under the name "detectLiveness".
|
|
14
14
|
// JS side calls VisionCameraProxy.initFrameProcessorPlugin('detectLiveness').
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// Lambda ignores proxy/options — FrameProcessorPlugin is no-arg in VC v4.5+.
|
|
16
|
+
FrameProcessorPluginRegistry.addFrameProcessorPlugin("detectLiveness") { _, _ ->
|
|
17
|
+
LivenessCameraPlugin()
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
}
|
|
@@ -7,12 +7,8 @@ import com.google.mlkit.vision.face.FaceDetection
|
|
|
7
7
|
import com.google.mlkit.vision.face.FaceDetectorOptions
|
|
8
8
|
import com.mrousavy.camera.frameprocessors.Frame
|
|
9
9
|
import com.mrousavy.camera.frameprocessors.FrameProcessorPlugin
|
|
10
|
-
import com.mrousavy.camera.frameprocessors.VisionCameraProxyHolder
|
|
11
10
|
|
|
12
|
-
class LivenessCameraPlugin(
|
|
13
|
-
proxy: VisionCameraProxyHolder,
|
|
14
|
-
options: Map<String, Any>?
|
|
15
|
-
) : FrameProcessorPlugin(proxy, options) {
|
|
11
|
+
class LivenessCameraPlugin : FrameProcessorPlugin() {
|
|
16
12
|
|
|
17
13
|
private val faceDetector = FaceDetection.getClient(
|
|
18
14
|
FaceDetectorOptions.Builder()
|
|
@@ -23,15 +19,27 @@ class LivenessCameraPlugin(
|
|
|
23
19
|
.build()
|
|
24
20
|
)
|
|
25
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Convert Vision Camera's Orientation to ML Kit rotation degrees.
|
|
24
|
+
* Uses toString() to stay resilient across VC patch-version API churn.
|
|
25
|
+
*/
|
|
26
|
+
private fun orientationDegrees(frame: Frame): Int {
|
|
27
|
+
val name = frame.orientation.toString().uppercase()
|
|
28
|
+
return when {
|
|
29
|
+
name.contains("LANDSCAPE_LEFT") -> 90
|
|
30
|
+
name.contains("PORTRAIT_UPSIDE_DOWN") -> 180
|
|
31
|
+
name.contains("LANDSCAPE_RIGHT") -> 270
|
|
32
|
+
else -> 0 // PORTRAIT
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
override fun callback(frame: Frame, arguments: Map<String, Any>?): Any {
|
|
27
37
|
val mediaImage: Image = frame.image
|
|
28
|
-
val
|
|
29
|
-
|
|
30
|
-
val inputImage = InputImage.fromMediaImage(mediaImage, rotationDegrees)
|
|
38
|
+
val inputImage = InputImage.fromMediaImage(mediaImage, orientationDegrees(frame))
|
|
31
39
|
|
|
32
40
|
return try {
|
|
33
41
|
// Tasks.await blocks the frame-processor thread synchronously.
|
|
34
|
-
// ML Kit face detection is fast (~5
|
|
42
|
+
// ML Kit face detection is fast (~5–10 ms) so this is acceptable.
|
|
35
43
|
val faces = Tasks.await(faceDetector.process(inputImage))
|
|
36
44
|
|
|
37
45
|
if (faces.isEmpty()) {
|
|
@@ -41,20 +49,23 @@ class LivenessCameraPlugin(
|
|
|
41
49
|
val face = faces.first()
|
|
42
50
|
val box = face.boundingBox
|
|
43
51
|
|
|
52
|
+
// IMPORTANT: All numeric values must be Double, not Float.
|
|
53
|
+
// JSI (Vision Camera's JS bridge) cannot convert Java Float → jsi::Value
|
|
54
|
+
// and throws "Cannot convert Java type class java.lang.Float" at runtime.
|
|
44
55
|
mapOf(
|
|
45
56
|
"detected" to true,
|
|
46
57
|
"bounds" to mapOf(
|
|
47
|
-
"x" to box.left.
|
|
48
|
-
"y" to box.top.
|
|
49
|
-
"width" to box.width().
|
|
50
|
-
"height" to box.height().
|
|
58
|
+
"x" to box.left.toDouble(),
|
|
59
|
+
"y" to box.top.toDouble(),
|
|
60
|
+
"width" to box.width().toDouble(),
|
|
61
|
+
"height" to box.height().toDouble()
|
|
51
62
|
),
|
|
52
|
-
"yawAngle" to face.headEulerAngleY,
|
|
53
|
-
"pitchAngle" to face.headEulerAngleX,
|
|
54
|
-
"rollAngle" to face.headEulerAngleZ,
|
|
55
|
-
"leftEyeOpenProbability" to (face.leftEyeOpenProbability ?: -
|
|
56
|
-
"rightEyeOpenProbability" to (face.rightEyeOpenProbability ?: -
|
|
57
|
-
"smilingProbability" to (face.smilingProbability ?: -
|
|
63
|
+
"yawAngle" to face.headEulerAngleY.toDouble(),
|
|
64
|
+
"pitchAngle" to face.headEulerAngleX.toDouble(),
|
|
65
|
+
"rollAngle" to face.headEulerAngleZ.toDouble(),
|
|
66
|
+
"leftEyeOpenProbability" to (face.leftEyeOpenProbability?.toDouble() ?: -1.0),
|
|
67
|
+
"rightEyeOpenProbability" to (face.rightEyeOpenProbability?.toDouble() ?: -1.0),
|
|
68
|
+
"smilingProbability" to (face.smilingProbability?.toDouble() ?: -1.0)
|
|
58
69
|
)
|
|
59
70
|
} catch (e: Exception) {
|
|
60
71
|
mapOf("detected" to false)
|
|
@@ -3,23 +3,36 @@
|
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { Animated, StyleSheet, Text, View } from 'react-native';
|
|
5
5
|
import { Camera, useCameraDevice, useCameraPermission } from 'react-native-vision-camera';
|
|
6
|
-
import {
|
|
6
|
+
import { Ellipse, Path, Svg } from 'react-native-svg';
|
|
7
7
|
import { useLivenessCamera } from "./useLivenessCamera.js";
|
|
8
8
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// Oval is sized relative to container WIDTH only, so it stays face-shaped
|
|
10
|
+
// on any screen. ry = rx * FACE_RATIO gives a natural portrait face oval.
|
|
11
|
+
const OVAL_WIDTH_RATIO = 0.72; // oval width = 72 % of container width
|
|
12
|
+
const FACE_RATIO = 1.35; // height-to-width ratio of the oval (~3:4 face)
|
|
11
13
|
const STROKE_WIDTH = 3;
|
|
12
14
|
// Cubic bezier approximation constant for a smooth ellipse
|
|
13
15
|
const K = 0.5523;
|
|
14
|
-
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns the stroke colour for the oval guide.
|
|
19
|
+
*
|
|
20
|
+
* ● White – no face / scanning (score < 0.4)
|
|
21
|
+
* ● Yellow – face detected, confidence building (0.4 ≤ score < threshold)
|
|
22
|
+
* ● Green – liveness confirmed / countdown / capture
|
|
23
|
+
* ● Red – error
|
|
24
|
+
*/
|
|
25
|
+
function getOvalColor(state, score) {
|
|
15
26
|
switch (state) {
|
|
27
|
+
case 'error':
|
|
28
|
+
return '#FF3B30';
|
|
16
29
|
case 'confirmed':
|
|
17
30
|
case 'countdown':
|
|
18
31
|
case 'capturing':
|
|
19
32
|
case 'done':
|
|
20
33
|
return '#4CAF50';
|
|
21
34
|
default:
|
|
22
|
-
return '#FFFFFF';
|
|
35
|
+
return score >= 0.4 ? '#FFD60A' : '#FFFFFF';
|
|
23
36
|
}
|
|
24
37
|
}
|
|
25
38
|
|
|
@@ -34,18 +47,19 @@ function ellipsePath(cx, cy, rx, ry) {
|
|
|
34
47
|
function OvalOverlay({
|
|
35
48
|
width,
|
|
36
49
|
height,
|
|
37
|
-
state
|
|
50
|
+
state,
|
|
51
|
+
score
|
|
38
52
|
}) {
|
|
39
53
|
if (width === 0 || height === 0) return null;
|
|
40
54
|
const cx = width / 2;
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
55
|
+
// Shift centre slightly above midpoint so the face sits naturally in frame
|
|
56
|
+
const cy = height * 0.45;
|
|
57
|
+
const rx = width * OVAL_WIDTH_RATIO / 2;
|
|
58
|
+
const ry = rx * FACE_RATIO;
|
|
59
|
+
const color = getOvalColor(state, score);
|
|
45
60
|
|
|
46
61
|
// Compound path: outer rect + oval. evenodd fill rule makes the oval transparent.
|
|
47
62
|
const scrimD = `M0 0H${width}V${height}H0Z ${ellipsePath(cx, cy, rx, ry)}`;
|
|
48
|
-
const showDot = state === 'confirmed' || state === 'countdown' || state === 'capturing';
|
|
49
63
|
return /*#__PURE__*/_jsxs(Svg, {
|
|
50
64
|
style: StyleSheet.absoluteFill,
|
|
51
65
|
width: width,
|
|
@@ -62,11 +76,6 @@ function OvalOverlay({
|
|
|
62
76
|
fill: "none",
|
|
63
77
|
stroke: color,
|
|
64
78
|
strokeWidth: STROKE_WIDTH
|
|
65
|
-
}), showDot && /*#__PURE__*/_jsx(Circle, {
|
|
66
|
-
cx: cx,
|
|
67
|
-
cy: cy - ry - 8,
|
|
68
|
-
r: 5,
|
|
69
|
-
fill: color
|
|
70
79
|
})]
|
|
71
80
|
});
|
|
72
81
|
}
|
|
@@ -138,6 +147,7 @@ export function LivenessCamera({
|
|
|
138
147
|
const {
|
|
139
148
|
frameProcessor,
|
|
140
149
|
livenessState,
|
|
150
|
+
livenessScore,
|
|
141
151
|
countdown,
|
|
142
152
|
feedback
|
|
143
153
|
} = useLivenessCamera({
|
|
@@ -199,7 +209,8 @@ export function LivenessCamera({
|
|
|
199
209
|
}), /*#__PURE__*/_jsx(OvalOverlay, {
|
|
200
210
|
width: containerSize.width,
|
|
201
211
|
height: containerSize.height,
|
|
202
|
-
state: livenessState
|
|
212
|
+
state: livenessState,
|
|
213
|
+
score: livenessScore
|
|
203
214
|
}), livenessState !== 'done' && /*#__PURE__*/_jsx(View, {
|
|
204
215
|
style: styles.feedbackContainer,
|
|
205
216
|
children: /*#__PURE__*/_jsx(Text, {
|
|
@@ -235,7 +246,7 @@ const styles = StyleSheet.create({
|
|
|
235
246
|
},
|
|
236
247
|
feedbackContainer: {
|
|
237
248
|
position: 'absolute',
|
|
238
|
-
bottom: '
|
|
249
|
+
bottom: '12%',
|
|
239
250
|
left: 0,
|
|
240
251
|
right: 0,
|
|
241
252
|
alignItems: 'center',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["useCallback","useEffect","useRef","useState","Animated","StyleSheet","Text","View","Camera","useCameraDevice","useCameraPermission","
|
|
1
|
+
{"version":3,"names":["useCallback","useEffect","useRef","useState","Animated","StyleSheet","Text","View","Camera","useCameraDevice","useCameraPermission","Ellipse","Path","Svg","useLivenessCamera","jsx","_jsx","jsxs","_jsxs","OVAL_WIDTH_RATIO","FACE_RATIO","STROKE_WIDTH","K","getOvalColor","state","score","ellipsePath","cx","cy","rx","ry","join","OvalOverlay","width","height","color","scrimD","style","absoluteFill","children","d","fill","fillRule","stroke","strokeWidth","CountdownBubble","value","scale","Value","current","opacity","parallel","sequence","spring","toValue","stiffness","damping","useNativeDriver","timing","duration","start","styles","countdownBubble","transform","countdownText","LivenessCamera","onCapture","onLivenessConfirmed","onError","countdownFrom","livenessThreshold","confirmFrames","soundEnabled","hasPermission","requestPermission","device","cameraRef","containerSize","setContainerSize","frameProcessor","livenessState","livenessScore","countdown","feedback","handleLayout","e","nativeEvent","layout","catch","Error","root","centered","permissionText","onLayout","ref","isActive","photo","pixelFormat","feedbackContainer","feedbackText","countdownContainer","captureFlash","pointerEvents","create","flex","backgroundColor","overflow","justifyContent","alignItems","fontSize","textAlign","paddingHorizontal","position","bottom","left","right","fontWeight","textShadowColor","textShadowOffset","textShadowRadius","absoluteFillObject","borderRadius","borderWidth","borderColor","lineHeight"],"sourceRoot":"../../src","sources":["LivenessCamera.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LivenessCamera.d.ts","sourceRoot":"","sources":["../../../src/LivenessCamera.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,mBAAmB,EAAiB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"LivenessCamera.d.ts","sourceRoot":"","sources":["../../../src/LivenessCamera.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,mBAAmB,EAAiB,MAAM,SAAS,CAAC;AAsIlE,wBAAgB,cAAc,CAAC,EAC7B,SAAS,EACT,mBAAmB,EACnB,OAAO,EACP,aAAiB,EACjB,iBAAwB,EACxB,aAAkB,EAClB,YAAmB,EACnB,KAAK,GACN,EAAE,mBAAmB,2CAkFrB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rick427/react-native-liveness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Liveness detection library for React Native using Vision Camera v4 and ML Kit",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
package/src/LivenessCamera.tsx
CHANGED
|
@@ -5,25 +5,37 @@ import {
|
|
|
5
5
|
useCameraDevice,
|
|
6
6
|
useCameraPermission,
|
|
7
7
|
} from 'react-native-vision-camera';
|
|
8
|
-
import {
|
|
8
|
+
import { Ellipse, Path, Svg } from 'react-native-svg';
|
|
9
9
|
import { useLivenessCamera } from './useLivenessCamera';
|
|
10
10
|
import type { LivenessCameraProps, LivenessState } from './types';
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// Oval is sized relative to container WIDTH only, so it stays face-shaped
|
|
13
|
+
// on any screen. ry = rx * FACE_RATIO gives a natural portrait face oval.
|
|
14
|
+
const OVAL_WIDTH_RATIO = 0.72; // oval width = 72 % of container width
|
|
15
|
+
const FACE_RATIO = 1.35; // height-to-width ratio of the oval (~3:4 face)
|
|
14
16
|
const STROKE_WIDTH = 3;
|
|
15
17
|
// Cubic bezier approximation constant for a smooth ellipse
|
|
16
18
|
const K = 0.5523;
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Returns the stroke colour for the oval guide.
|
|
22
|
+
*
|
|
23
|
+
* ● White – no face / scanning (score < 0.4)
|
|
24
|
+
* ● Yellow – face detected, confidence building (0.4 ≤ score < threshold)
|
|
25
|
+
* ● Green – liveness confirmed / countdown / capture
|
|
26
|
+
* ● Red – error
|
|
27
|
+
*/
|
|
28
|
+
function getOvalColor(state: LivenessState, score: number): string {
|
|
19
29
|
switch (state) {
|
|
30
|
+
case 'error':
|
|
31
|
+
return '#FF3B30';
|
|
20
32
|
case 'confirmed':
|
|
21
33
|
case 'countdown':
|
|
22
34
|
case 'capturing':
|
|
23
35
|
case 'done':
|
|
24
36
|
return '#4CAF50';
|
|
25
37
|
default:
|
|
26
|
-
return '#FFFFFF';
|
|
38
|
+
return score >= 0.4 ? '#FFD60A' : '#FFFFFF';
|
|
27
39
|
}
|
|
28
40
|
}
|
|
29
41
|
|
|
@@ -47,25 +59,25 @@ function OvalOverlay({
|
|
|
47
59
|
width,
|
|
48
60
|
height,
|
|
49
61
|
state,
|
|
62
|
+
score,
|
|
50
63
|
}: {
|
|
51
64
|
width: number;
|
|
52
65
|
height: number;
|
|
53
66
|
state: LivenessState;
|
|
67
|
+
score: number;
|
|
54
68
|
}) {
|
|
55
69
|
if (width === 0 || height === 0) return null;
|
|
56
70
|
|
|
57
71
|
const cx = width / 2;
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
72
|
+
// Shift centre slightly above midpoint so the face sits naturally in frame
|
|
73
|
+
const cy = height * 0.45;
|
|
74
|
+
const rx = (width * OVAL_WIDTH_RATIO) / 2;
|
|
75
|
+
const ry = rx * FACE_RATIO;
|
|
76
|
+
const color = getOvalColor(state, score);
|
|
62
77
|
|
|
63
78
|
// Compound path: outer rect + oval. evenodd fill rule makes the oval transparent.
|
|
64
79
|
const scrimD = `M0 0H${width}V${height}H0Z ${ellipsePath(cx, cy, rx, ry)}`;
|
|
65
80
|
|
|
66
|
-
const showDot =
|
|
67
|
-
state === 'confirmed' || state === 'countdown' || state === 'capturing';
|
|
68
|
-
|
|
69
81
|
return (
|
|
70
82
|
<Svg style={StyleSheet.absoluteFill} width={width} height={height}>
|
|
71
83
|
<Path d={scrimD} fill="rgba(0,0,0,0.55)" fillRule="evenodd" />
|
|
@@ -78,7 +90,6 @@ function OvalOverlay({
|
|
|
78
90
|
stroke={color}
|
|
79
91
|
strokeWidth={STROKE_WIDTH}
|
|
80
92
|
/>
|
|
81
|
-
{showDot && <Circle cx={cx} cy={cy - ry - 8} r={5} fill={color} />}
|
|
82
93
|
</Svg>
|
|
83
94
|
);
|
|
84
95
|
}
|
|
@@ -145,7 +156,7 @@ export function LivenessCamera({
|
|
|
145
156
|
const cameraRef = useRef<Camera>(null);
|
|
146
157
|
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
|
147
158
|
|
|
148
|
-
const { frameProcessor, livenessState, countdown, feedback } =
|
|
159
|
+
const { frameProcessor, livenessState, livenessScore, countdown, feedback } =
|
|
149
160
|
useLivenessCamera({
|
|
150
161
|
livenessThreshold,
|
|
151
162
|
confirmFrames,
|
|
@@ -204,6 +215,7 @@ export function LivenessCamera({
|
|
|
204
215
|
width={containerSize.width}
|
|
205
216
|
height={containerSize.height}
|
|
206
217
|
state={livenessState}
|
|
218
|
+
score={livenessScore}
|
|
207
219
|
/>
|
|
208
220
|
{livenessState !== 'done' && (
|
|
209
221
|
<View style={styles.feedbackContainer}>
|
|
@@ -240,7 +252,7 @@ const styles = StyleSheet.create({
|
|
|
240
252
|
},
|
|
241
253
|
feedbackContainer: {
|
|
242
254
|
position: 'absolute',
|
|
243
|
-
bottom: '
|
|
255
|
+
bottom: '12%',
|
|
244
256
|
left: 0,
|
|
245
257
|
right: 0,
|
|
246
258
|
alignItems: 'center',
|