react-native-vision-camera-spoof-detector 1.1.10 → 1.1.15
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/README.md +103 -186
- package/VisionCameraSpoofDetector.podspec +41 -0
- package/ios/FaceAntiSpoofManager.swift +106 -4
- package/ios/FaceAntiSpoofModule.m +38 -51
- package/ios/HybridSpoofDetectorFactory.swift +10 -0
- package/ios/HybridSpoofDetectorOutput.swift +164 -0
- package/nitro.json +4 -0
- package/nitrogen/generated/ios/VisionCameraSpoofDetector+autolinking.rb +62 -0
- package/nitrogen/generated/ios/VisionCameraSpoofDetector-Swift-Cxx-Bridge.cpp +60 -0
- package/nitrogen/generated/ios/VisionCameraSpoofDetector-Swift-Cxx-Bridge.hpp +151 -0
- package/nitrogen/generated/ios/VisionCameraSpoofDetector-Swift-Cxx-Umbrella.hpp +60 -0
- package/nitrogen/generated/ios/VisionCameraSpoofDetectorAutolinking.mm +33 -0
- package/nitrogen/generated/ios/VisionCameraSpoofDetectorAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridSpoofDetectorFactorySpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridSpoofDetectorFactorySpecSwift.hpp +97 -0
- package/nitrogen/generated/ios/swift/AntiSpoofingResult.swift +54 -0
- package/nitrogen/generated/ios/swift/Func_void_AntiSpoofingResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridSpoofDetectorFactorySpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridSpoofDetectorFactorySpec_cxx.swift +142 -0
- package/nitrogen/generated/ios/swift/Size.swift +34 -0
- package/nitrogen/generated/ios/swift/SpoofDetectorOutputOptions.swift +74 -0
- package/package.json +8 -3
- package/src/specs/SpoofDetectorFactory.nitro.ts +1 -0
- package/ios/FaceAntiSpoofFrameProcessor.swift +0 -301
- package/ios/FaceAntiSpoofJSI.h +0 -12
- package/ios/FaceAntiSpoofJSI.mm +0 -92
- package/ios/FaceAntiSpoofPluginRegister.m +0 -84
- package/ios/models/FaceAntiSpoofing.tflite +0 -0
- package/react-native-vision-camera-spoof-detector.podspec +0 -22
- /package/{ios/models → lib/assets}/model_spec.json +0 -0
package/README.md
CHANGED
|
@@ -19,13 +19,11 @@ High-performance face anti-spoofing and liveness detection module for React Nati
|
|
|
19
19
|
|
|
20
20
|
## 📋 Requirements
|
|
21
21
|
|
|
22
|
-
- React Native >= 0.
|
|
23
|
-
- React Native Vision Camera `^
|
|
24
|
-
- `react-native-reanimated` `^3.0.0`
|
|
25
|
-
- `react-native-worklets` `^0.12.0`
|
|
22
|
+
- React Native >= 0.76.0
|
|
23
|
+
- React Native Vision Camera `^5.0.0`
|
|
26
24
|
- `react-native-vision-camera-worklets` `^5.0.0`
|
|
27
25
|
- iOS 11.0 or later
|
|
28
|
-
- react-native-vision-camera-face-detector (
|
|
26
|
+
- `react-native-vision-camera-face-detector` (required for the face-gated example below)
|
|
29
27
|
|
|
30
28
|
## 📦 Installation
|
|
31
29
|
|
|
@@ -40,9 +38,9 @@ yarn add react-native-vision-camera-spoof-detector
|
|
|
40
38
|
### Step 2: Install peer dependencies
|
|
41
39
|
|
|
42
40
|
```bash
|
|
43
|
-
npm install react-native-vision-camera react-native-reanimated react-native-worklets react-native-vision-camera-worklets
|
|
41
|
+
npm install react-native-vision-camera react-native-reanimated react-native-worklets react-native-vision-camera-worklets react-native-vision-camera-face-detector
|
|
44
42
|
# or
|
|
45
|
-
yarn add react-native-vision-camera react-native-reanimated react-native-worklets react-native-vision-camera-worklets
|
|
43
|
+
yarn add react-native-vision-camera react-native-reanimated react-native-worklets react-native-vision-camera-worklets react-native-vision-camera-face-detector
|
|
46
44
|
```
|
|
47
45
|
|
|
48
46
|
### Step 3: Configure iOS
|
|
@@ -82,213 +80,144 @@ react-native link react-native-vision-camera-spoof-detector
|
|
|
82
80
|
|
|
83
81
|
## 🚀 Quick Start
|
|
84
82
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
### Simple Usage
|
|
83
|
+
Vision Camera 5 uses camera outputs for this package. The anti-spoof output can be combined with the face detector output and enabled only while a face is visible.
|
|
88
84
|
|
|
89
85
|
```javascript
|
|
90
86
|
import React, { useEffect, useState } from 'react';
|
|
91
|
-
import { StyleSheet, Text, View } from 'react-native';
|
|
92
|
-
import {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
87
|
+
import { StyleSheet, Text, useWindowDimensions, View } from 'react-native';
|
|
88
|
+
import {
|
|
89
|
+
Camera,
|
|
90
|
+
useCameraDevice,
|
|
91
|
+
useCameraPermission,
|
|
92
|
+
} from 'react-native-vision-camera';
|
|
93
|
+
import { useFaceDetectorOutput } from 'react-native-vision-camera-face-detector';
|
|
94
|
+
import { useFaceAntiSpoofFrameOutput } from 'react-native-vision-camera-spoof-detector';
|
|
95
|
+
|
|
96
|
+
const CameraScreen = () => {
|
|
97
|
+
const device = useCameraDevice('front');
|
|
99
98
|
const { hasPermission, requestPermission } = useCameraPermission();
|
|
99
|
+
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
|
100
|
+
const [faces, setFaces] = useState([]);
|
|
101
|
+
const [detectionError, setDetectionError] = useState(null);
|
|
100
102
|
const [spoofResult, setSpoofResult] = useState(null);
|
|
101
103
|
|
|
102
104
|
useEffect(() => {
|
|
103
105
|
if (!hasPermission) requestPermission();
|
|
104
106
|
}, [hasPermission, requestPermission]);
|
|
105
107
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
const faceDetectorOutput = useFaceDetectorOutput({
|
|
109
|
+
cameraFacing: 'front',
|
|
110
|
+
autoMode: true,
|
|
111
|
+
windowWidth,
|
|
112
|
+
windowHeight,
|
|
113
|
+
performanceMode: 'fast',
|
|
114
|
+
trackingEnabled: true,
|
|
115
|
+
onFacesDetected(detectedFaces) {
|
|
116
|
+
setDetectionError(null);
|
|
117
|
+
setFaces(detectedFaces);
|
|
118
|
+
if (detectedFaces.length === 0) setSpoofResult(null);
|
|
119
|
+
},
|
|
120
|
+
onError(error) {
|
|
121
|
+
setDetectionError(error.message);
|
|
122
|
+
},
|
|
123
|
+
});
|
|
111
124
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
}, []);
|
|
125
|
+
const antiSpoofOutput = useFaceAntiSpoofFrameOutput({
|
|
126
|
+
onResult(result) {
|
|
127
|
+
setSpoofResult(result);
|
|
128
|
+
},
|
|
129
|
+
});
|
|
119
130
|
|
|
120
|
-
|
|
131
|
+
const outputs = faces.length > 0
|
|
132
|
+
? [faceDetectorOutput, antiSpoofOutput]
|
|
133
|
+
: [faceDetectorOutput];
|
|
134
|
+
|
|
135
|
+
if (!hasPermission) return <Message text="Camera permission is required." />;
|
|
136
|
+
if (!device) return <Message text="No front camera is available." />;
|
|
121
137
|
|
|
122
138
|
return (
|
|
123
139
|
<View style={styles.container}>
|
|
124
140
|
<Camera
|
|
125
141
|
style={StyleSheet.absoluteFill}
|
|
126
142
|
device={device}
|
|
127
|
-
isActive
|
|
128
|
-
|
|
129
|
-
frameProcessorFps={5}
|
|
143
|
+
isActive
|
|
144
|
+
outputs={outputs}
|
|
130
145
|
/>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
</
|
|
143
|
-
|
|
146
|
+
|
|
147
|
+
<View style={styles.statusContainer}>
|
|
148
|
+
<Text style={styles.statusText}>
|
|
149
|
+
{detectionError ?? `${faces.length} face${faces.length === 1 ? '' : 's'} detected`}
|
|
150
|
+
</Text>
|
|
151
|
+
<Text style={styles.statusText}>
|
|
152
|
+
{faces.length === 0
|
|
153
|
+
? 'Waiting for face...'
|
|
154
|
+
: spoofResult
|
|
155
|
+
? JSON.stringify(spoofResult)
|
|
156
|
+
: 'Anti-spoof processing...'}
|
|
157
|
+
</Text>
|
|
158
|
+
</View>
|
|
144
159
|
</View>
|
|
145
160
|
);
|
|
146
|
-
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const Message = ({ text }) => (
|
|
164
|
+
<View style={styles.messageContainer}>
|
|
165
|
+
<Text style={styles.messageText}>{text}</Text>
|
|
166
|
+
</View>
|
|
167
|
+
);
|
|
147
168
|
|
|
148
169
|
const styles = StyleSheet.create({
|
|
149
170
|
container: { flex: 1 },
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
bottom: 50,
|
|
153
|
-
left: 0,
|
|
154
|
-
right: 0,
|
|
171
|
+
messageContainer: {
|
|
172
|
+
flex: 1,
|
|
155
173
|
alignItems: 'center',
|
|
156
|
-
|
|
157
|
-
|
|
174
|
+
justifyContent: 'center',
|
|
175
|
+
paddingHorizontal: 24,
|
|
158
176
|
},
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
177
|
+
messageText: { fontSize: 16, textAlign: 'center' },
|
|
178
|
+
statusContainer: {
|
|
179
|
+
position: 'absolute',
|
|
180
|
+
top: 56,
|
|
181
|
+
alignSelf: 'center',
|
|
182
|
+
paddingHorizontal: 14,
|
|
183
|
+
paddingVertical: 8,
|
|
184
|
+
borderRadius: 16,
|
|
185
|
+
backgroundColor: 'rgba(0, 0, 0, 0.65)',
|
|
163
186
|
},
|
|
187
|
+
statusText: { color: '#ffffff', fontSize: 14, marginVertical: 2 },
|
|
164
188
|
});
|
|
189
|
+
|
|
190
|
+
export default CameraScreen;
|
|
165
191
|
```
|
|
166
192
|
|
|
167
|
-
|
|
193
|
+
The anti-spoof output callback receives a result after native inference. No manual `runOnJS`, frame processor, or explicit initialization call is required for this output API. The output may be omitted until a face is detected, as shown above.
|
|
168
194
|
|
|
169
|
-
|
|
170
|
-
import { useCallback, useMemo, useEffect, useRef } from 'react';
|
|
171
|
-
import { scheduleOnRN } from 'react-native-worklets';
|
|
172
|
-
import { useFrameProcessor } from 'react-native-vision-camera';
|
|
173
|
-
import { useFaceDetector } from 'react-native-vision-camera-face-detector';
|
|
174
|
-
import {
|
|
175
|
-
faceAntiSpoofFrameProcessor,
|
|
176
|
-
initializeFaceAntiSpoof,
|
|
177
|
-
isFaceAntiSpoofAvailable,
|
|
178
|
-
} from 'react-native-vision-camera-spoof-detector';
|
|
179
|
-
|
|
180
|
-
const useFaceDetectionFrameProcessor = ({
|
|
181
|
-
onStableFaceDetected = () => { },
|
|
182
|
-
onFacesUpdate = () => { },
|
|
183
|
-
onLivenessUpdate = () => { },
|
|
184
|
-
onAntiSpoofUpdate = () => { },
|
|
185
|
-
showCodeScanner = false,
|
|
186
|
-
isLoading = false,
|
|
187
|
-
isActive = true,
|
|
188
|
-
livenessLevel = 0,
|
|
189
|
-
antispooflevel = 0.35,
|
|
190
|
-
}) => {
|
|
191
|
-
const { detectFaces } = useFaceDetector({
|
|
192
|
-
performanceMode: 'fast',
|
|
193
|
-
landmarkMode: 'none',
|
|
194
|
-
contourMode: 'none',
|
|
195
|
-
classificationMode: livenessLevel === 1 ? 'all' : 'none',
|
|
196
|
-
minFaceSize: 0.2,
|
|
197
|
-
});
|
|
195
|
+
## 📚 API Reference
|
|
198
196
|
|
|
199
|
-
|
|
200
|
-
const antiSpoofInitialized = useRef(false);
|
|
201
|
-
|
|
202
|
-
const initializeAntiSpoof = useCallback(async () => {
|
|
203
|
-
if (antiSpoofInitialized.current) return true;
|
|
204
|
-
try {
|
|
205
|
-
const available = isFaceAntiSpoofAvailable?.();
|
|
206
|
-
if (!available) return false;
|
|
207
|
-
await initializeFaceAntiSpoof();
|
|
208
|
-
antiSpoofInitialized.current = true;
|
|
209
|
-
return true;
|
|
210
|
-
} catch (err) {
|
|
211
|
-
console.error('Anti-spoof initialization error:', err);
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
214
|
-
}, []);
|
|
197
|
+
### `useFaceAntiSpoofFrameOutput(options?)`
|
|
215
198
|
|
|
216
|
-
|
|
217
|
-
initializeAntiSpoof();
|
|
218
|
-
}, [initializeAntiSpoof]);
|
|
219
|
-
|
|
220
|
-
// Shared state for face tracking
|
|
221
|
-
const sharedState = useMemo(
|
|
222
|
-
() =>
|
|
223
|
-
Worklets.createSharedValue({
|
|
224
|
-
flags: {
|
|
225
|
-
captured: false,
|
|
226
|
-
showCodeScanner: showCodeScanner,
|
|
227
|
-
isActive: isActive,
|
|
228
|
-
hasSingleFace: false,
|
|
229
|
-
isFaceCentered: false,
|
|
230
|
-
},
|
|
231
|
-
antiSpoof: {
|
|
232
|
-
isLive: false,
|
|
233
|
-
confidence: 0,
|
|
234
|
-
consecutiveLiveFrames: 0,
|
|
235
|
-
},
|
|
236
|
-
}),
|
|
237
|
-
[]
|
|
238
|
-
);
|
|
199
|
+
Creates a Vision Camera 5 camera output for native anti-spoofing inference.
|
|
239
200
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (detected.length === 1 && !sharedState.value.flags.captured) {
|
|
253
|
-
const antiSpoofResult = faceAntiSpoofFrameProcessor?.(frame);
|
|
254
|
-
|
|
255
|
-
if (antiSpoofResult?.isLive) {
|
|
256
|
-
sharedState.value.antiSpoof.isLive = true;
|
|
257
|
-
sharedState.value.antiSpoof.confidence = antiSpoofResult.combinedScore;
|
|
258
|
-
onAntiSpoofUpdate?.({
|
|
259
|
-
isLive: true,
|
|
260
|
-
confidence: antiSpoofResult.combinedScore,
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
onFacesUpdate?.({ count: 1, progress: 50 });
|
|
265
|
-
} else {
|
|
266
|
-
onFacesUpdate?.({ count: detected.length, progress: 0 });
|
|
267
|
-
}
|
|
268
|
-
} catch (err) {
|
|
269
|
-
console.error('Frame processing error:', err);
|
|
270
|
-
} finally {
|
|
271
|
-
frame.release?.();
|
|
272
|
-
}
|
|
273
|
-
},
|
|
274
|
-
[detectFaces, isLoading]
|
|
275
|
-
);
|
|
201
|
+
```javascript
|
|
202
|
+
const antiSpoofOutput = useFaceAntiSpoofFrameOutput({
|
|
203
|
+
onResult(result) {
|
|
204
|
+
console.log(result);
|
|
205
|
+
},
|
|
206
|
+
onError(error) {
|
|
207
|
+
console.error(error);
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
```
|
|
276
211
|
|
|
277
|
-
|
|
278
|
-
frameProcessor,
|
|
279
|
-
sharedState,
|
|
280
|
-
initializeAntiSpoof,
|
|
281
|
-
};
|
|
282
|
-
};
|
|
212
|
+
Pass the returned output in the `Camera` component's `outputs` array. The callback runs on the React Native runtime after native inference.
|
|
283
213
|
|
|
284
|
-
|
|
285
|
-
```
|
|
214
|
+
**Options**: `onResult(result)` and optional `onError(error)`.
|
|
286
215
|
|
|
287
|
-
|
|
216
|
+
---
|
|
288
217
|
|
|
289
218
|
### `initializeFaceAntiSpoof()`
|
|
290
219
|
|
|
291
|
-
Initializes the
|
|
220
|
+
Initializes the native module when an application needs an explicit startup check or model-loading diagnostics. It is not required when using `useFaceAntiSpoofFrameOutput()`.
|
|
292
221
|
|
|
293
222
|
```javascript
|
|
294
223
|
const success = await initializeFaceAntiSpoof();
|
|
@@ -310,18 +239,6 @@ const available = isFaceAntiSpoofAvailable();
|
|
|
310
239
|
|
|
311
240
|
---
|
|
312
241
|
|
|
313
|
-
### `faceAntiSpoofFrameProcessor(frame)`
|
|
314
|
-
|
|
315
|
-
Process frame and get anti-spoofing result.
|
|
316
|
-
|
|
317
|
-
```javascript
|
|
318
|
-
const result = faceAntiSpoofFrameProcessor(frame);
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
**Parameters**: `frame` (Vision Camera Frame)
|
|
322
|
-
|
|
323
|
-
**Returns**: `FaceAntiSpoofingResult | null`. On iOS, inference runs off the frame callback and the returned object can be the latest completed result.
|
|
324
|
-
|
|
325
242
|
### `FaceAntiSpoofingResult`
|
|
326
243
|
|
|
327
244
|
```typescript
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'VisionCameraSpoofDetector'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.homepage = package['homepage']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.authors = package['author']
|
|
12
|
+
s.platform = :ios, '15.5'
|
|
13
|
+
s.source = { :path => '.' }
|
|
14
|
+
s.swift_version = '5.0'
|
|
15
|
+
s.pod_target_xcconfig = {
|
|
16
|
+
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64',
|
|
17
|
+
'HEADER_SEARCH_PATHS' => '$(inherited) ${PODS_TARGET_SRCROOT}/nitrogen/generated/shared/c++ ${PODS_TARGET_SRCROOT}/nitrogen/generated/ios ${PODS_TARGET_SRCROOT}/nitrogen/generated/ios/c++'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
load 'nitrogen/generated/ios/VisionCameraSpoofDetector+autolinking.rb'
|
|
21
|
+
add_nitrogen_files(s)
|
|
22
|
+
|
|
23
|
+
s.source_files = [
|
|
24
|
+
'ios/**/*.{h,m,mm,swift,cpp,c,cc}',
|
|
25
|
+
'nitrogen/generated/shared/**/*.cpp',
|
|
26
|
+
'nitrogen/generated/ios/**/*.{cpp,mm,swift}',
|
|
27
|
+
'nitrogen/generated/ios/VisionCameraSpoofDetector-Swift-Cxx-Bridge.hpp',
|
|
28
|
+
]
|
|
29
|
+
s.public_header_files = 'nitrogen/generated/ios/VisionCameraSpoofDetector-Swift-Cxx-Bridge.hpp'
|
|
30
|
+
s.private_header_files = [
|
|
31
|
+
'nitrogen/generated/shared/**/*.hpp',
|
|
32
|
+
'nitrogen/generated/ios/c++/**/*.{h,hpp}',
|
|
33
|
+
]
|
|
34
|
+
s.resources = ['lib/assets/*']
|
|
35
|
+
|
|
36
|
+
s.dependency 'TensorFlowLiteSwift'
|
|
37
|
+
s.dependency 'VisionCamera'
|
|
38
|
+
s.dependency 'React-jsi'
|
|
39
|
+
s.dependency 'React-callinvoker'
|
|
40
|
+
install_modules_dependencies(s)
|
|
41
|
+
end
|
|
@@ -6,8 +6,17 @@ import TensorFlowLite
|
|
|
6
6
|
|
|
7
7
|
@objcMembers
|
|
8
8
|
public class FaceAntiSpoofManager: NSObject {
|
|
9
|
+
public struct DetectionResult {
|
|
10
|
+
let isLive: Bool
|
|
11
|
+
let neuralNetworkScore: Double
|
|
12
|
+
let laplacianScore: Double
|
|
13
|
+
let combinedScore: Double
|
|
14
|
+
let confidence: Double
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
#if canImport(TensorFlowLite)
|
|
10
18
|
public static var sharedInterpreter: Interpreter?
|
|
19
|
+
private static let interpreterLock = NSLock()
|
|
11
20
|
#endif
|
|
12
21
|
|
|
13
22
|
@objc public class func initializeModel() -> Bool {
|
|
@@ -15,11 +24,19 @@ public class FaceAntiSpoofManager: NSObject {
|
|
|
15
24
|
if sharedInterpreter != nil { return true }
|
|
16
25
|
|
|
17
26
|
// Look in main bundle then plugin bundle
|
|
18
|
-
var modelPath = Bundle.main.path(
|
|
27
|
+
var modelPath = Bundle.main.path(
|
|
28
|
+
forResource: "FaceAntiSpoofing",
|
|
29
|
+
ofType: "tflite",
|
|
30
|
+
inDirectory: "lib/assets"
|
|
31
|
+
) ?? Bundle.main.path(forResource: "FaceAntiSpoofing", ofType: "tflite")
|
|
19
32
|
if modelPath == nil {
|
|
20
|
-
if let
|
|
21
|
-
let podBundle = Bundle(for:
|
|
22
|
-
modelPath = podBundle.path(
|
|
33
|
+
if let managerClass = NSClassFromString("FaceAntiSpoofManager") {
|
|
34
|
+
let podBundle = Bundle(for: managerClass)
|
|
35
|
+
modelPath = podBundle.path(
|
|
36
|
+
forResource: "FaceAntiSpoofing",
|
|
37
|
+
ofType: "tflite",
|
|
38
|
+
inDirectory: "lib/assets"
|
|
39
|
+
) ?? podBundle.path(forResource: "FaceAntiSpoofing", ofType: "tflite")
|
|
23
40
|
}
|
|
24
41
|
}
|
|
25
42
|
|
|
@@ -54,6 +71,91 @@ public class FaceAntiSpoofManager: NSObject {
|
|
|
54
71
|
#endif
|
|
55
72
|
}
|
|
56
73
|
|
|
74
|
+
public class func process(input: [Float]) throws -> DetectionResult {
|
|
75
|
+
#if canImport(TensorFlowLite)
|
|
76
|
+
if sharedInterpreter == nil && !initializeModel() {
|
|
77
|
+
throw NSError(domain: "FaceAntiSpoof", code: 3, userInfo: [
|
|
78
|
+
NSLocalizedDescriptionKey: "Unable to initialize the TensorFlow Lite interpreter"
|
|
79
|
+
])
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interpreterLock.lock()
|
|
83
|
+
defer { interpreterLock.unlock() }
|
|
84
|
+
|
|
85
|
+
guard let interpreter = sharedInterpreter else {
|
|
86
|
+
throw NSError(domain: "FaceAntiSpoof", code: 1, userInfo: [
|
|
87
|
+
NSLocalizedDescriptionKey: "Interpreter not available"
|
|
88
|
+
])
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let inputData = input.withUnsafeBufferPointer { buffer in
|
|
92
|
+
Data(buffer: buffer)
|
|
93
|
+
}
|
|
94
|
+
try interpreter.copy(inputData, toInputAt: 0)
|
|
95
|
+
try interpreter.invoke()
|
|
96
|
+
|
|
97
|
+
let classOutput = try interpreter.output(at: 0).data.withUnsafeBytes { buffer in
|
|
98
|
+
Array(buffer.bindMemory(to: Float.self))
|
|
99
|
+
}
|
|
100
|
+
let maskOutput = try interpreter.output(at: 1).data.withUnsafeBytes { buffer in
|
|
101
|
+
Array(buffer.bindMemory(to: Float.self))
|
|
102
|
+
}
|
|
103
|
+
let count = min(classOutput.count, maskOutput.count)
|
|
104
|
+
let neuralScore = (0..<count).reduce(Float.zero) { total, index in
|
|
105
|
+
total + abs(classOutput[index]) * maskOutput[index]
|
|
106
|
+
}
|
|
107
|
+
let laplacianScore = calculateLaplacianScore(input)
|
|
108
|
+
let combinedScore = calculateCombinedScore(neuralScore, laplacianScore)
|
|
109
|
+
let confidence = combinedScore
|
|
110
|
+
let isLive = neuralScore < 0.2 && laplacianScore > 1000
|
|
111
|
+
|
|
112
|
+
return DetectionResult(
|
|
113
|
+
isLive: isLive,
|
|
114
|
+
neuralNetworkScore: Double(neuralScore),
|
|
115
|
+
laplacianScore: Double(laplacianScore),
|
|
116
|
+
combinedScore: Double(combinedScore),
|
|
117
|
+
confidence: Double(confidence)
|
|
118
|
+
)
|
|
119
|
+
#else
|
|
120
|
+
throw NSError(domain: "FaceAntiSpoof", code: 2, userInfo: [
|
|
121
|
+
NSLocalizedDescriptionKey: "TensorFlowLite is not available"
|
|
122
|
+
])
|
|
123
|
+
#endif
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private class func calculateLaplacianScore(_ input: [Float]) -> Int {
|
|
127
|
+
let width = 256
|
|
128
|
+
let height = 256
|
|
129
|
+
var grey = [Int](repeating: 0, count: width * height)
|
|
130
|
+
for index in 0..<(width * height) {
|
|
131
|
+
let offset = index * 3
|
|
132
|
+
grey[index] = Int(
|
|
133
|
+
(0.299 * input[offset] + 0.587 * input[offset + 1] + 0.114 * input[offset + 2]) * 255
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let kernel = [[0, 1, 0], [1, -4, 1], [0, 1, 0]]
|
|
138
|
+
var score = 0
|
|
139
|
+
for y in 0..<(height - 2) {
|
|
140
|
+
for x in 0..<(width - 2) {
|
|
141
|
+
var result = 0
|
|
142
|
+
for ky in 0..<3 {
|
|
143
|
+
for kx in 0..<3 {
|
|
144
|
+
result += grey[(y + ky) * width + x + kx] * kernel[ky][kx]
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if result > 50 { score += 1 }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return score
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private class func calculateCombinedScore(_ neuralScore: Float, _ laplacianScore: Int) -> Float {
|
|
154
|
+
let neural = min(max(neuralScore, 0), 1)
|
|
155
|
+
let laplacian = min(max(Float(laplacianScore) / 1000, 0), 1)
|
|
156
|
+
return ((1 - neural) * 0.6) + (laplacian * 0.4)
|
|
157
|
+
}
|
|
158
|
+
|
|
57
159
|
@objc public class func cleanupModel() {
|
|
58
160
|
#if canImport(TensorFlowLite)
|
|
59
161
|
sharedInterpreter = nil
|