react-native-vision-camera-spoof-detector 1.1.11 → 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/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
|
@@ -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
|
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
#import "RCTBridge.h"
|
|
14
14
|
#endif
|
|
15
15
|
|
|
16
|
-
extern void FaceAntiSpoofRegisterPlugin(void);
|
|
17
|
-
|
|
18
16
|
@interface FaceAntiSpoofModule : NSObject <RCTBridgeModule>
|
|
19
17
|
@end
|
|
20
18
|
|
|
@@ -35,9 +33,7 @@ RCT_REMAP_METHOD(initialize,
|
|
|
35
33
|
initializeWithResolver:(RCTPromiseResolveBlock)resolve
|
|
36
34
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
37
35
|
{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
BOOL pluginAvailable = (NSClassFromString(@"FaceAntiSpoofFrameProcessor") != Nil);
|
|
36
|
+
BOOL pluginAvailable = (NSClassFromString(@"FaceAntiSpoofManager") != Nil);
|
|
41
37
|
|
|
42
38
|
// Prefer the FaceAntiSpoofManager to initialize the model and create the interpreter
|
|
43
39
|
BOOL managerInitialized = NO;
|
|
@@ -52,12 +48,18 @@ RCT_REMAP_METHOD(initialize,
|
|
|
52
48
|
}
|
|
53
49
|
|
|
54
50
|
// As a fallback, check bundle resources
|
|
55
|
-
NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
51
|
+
NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"FaceAntiSpoofing" ofType:@"tflite" inDirectory:@"lib/assets"];
|
|
52
|
+
if (!modelPath) {
|
|
53
|
+
modelPath = [[NSBundle mainBundle] pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
54
|
+
}
|
|
56
55
|
if (!modelPath) {
|
|
57
|
-
Class
|
|
58
|
-
if (
|
|
59
|
-
NSBundle *podBundle = [NSBundle bundleForClass:
|
|
60
|
-
modelPath = [podBundle pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
56
|
+
Class managerClass = NSClassFromString(@"FaceAntiSpoofManager");
|
|
57
|
+
if (managerClass != Nil) {
|
|
58
|
+
NSBundle *podBundle = [NSBundle bundleForClass:managerClass];
|
|
59
|
+
modelPath = [podBundle pathForResource:@"FaceAntiSpoofing" ofType:@"tflite" inDirectory:@"lib/assets"];
|
|
60
|
+
if (!modelPath) {
|
|
61
|
+
modelPath = [podBundle pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
62
|
+
}
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
BOOL modelLoaded = (modelPath != nil) || managerInitialized;
|
|
@@ -69,14 +71,19 @@ RCT_REMAP_METHOD(checkModelStatus,
|
|
|
69
71
|
checkModelStatusWithResolver:(RCTPromiseResolveBlock)resolve
|
|
70
72
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
71
73
|
{
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
74
|
+
BOOL pluginAvailable = (NSClassFromString(@"FaceAntiSpoofManager") != Nil);
|
|
75
|
+
NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"FaceAntiSpoofing" ofType:@"tflite" inDirectory:@"lib/assets"];
|
|
75
76
|
if (!modelPath) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
modelPath = [[NSBundle mainBundle] pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
78
|
+
}
|
|
79
|
+
if (!modelPath) {
|
|
80
|
+
Class managerClass = NSClassFromString(@"FaceAntiSpoofManager");
|
|
81
|
+
if (managerClass != Nil) {
|
|
82
|
+
NSBundle *podBundle = [NSBundle bundleForClass:managerClass];
|
|
83
|
+
modelPath = [podBundle pathForResource:@"FaceAntiSpoofing" ofType:@"tflite" inDirectory:@"lib/assets"];
|
|
84
|
+
if (!modelPath) {
|
|
85
|
+
modelPath = [podBundle pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
86
|
+
}
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
BOOL modelLoaded = (modelPath != nil);
|
|
@@ -88,7 +95,7 @@ RCT_REMAP_METHOD(isAvailable,
|
|
|
88
95
|
isAvailableWithResolver:(RCTPromiseResolveBlock)resolve
|
|
89
96
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
90
97
|
{
|
|
91
|
-
BOOL pluginAvailable = (NSClassFromString(@"
|
|
98
|
+
BOOL pluginAvailable = (NSClassFromString(@"FaceAntiSpoofManager") != Nil);
|
|
92
99
|
resolve(@(pluginAvailable));
|
|
93
100
|
}
|
|
94
101
|
|
|
@@ -96,7 +103,7 @@ RCT_REMAP_METHOD(testMethod,
|
|
|
96
103
|
testMethodWithResolver:(RCTPromiseResolveBlock)resolve
|
|
97
104
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
98
105
|
{
|
|
99
|
-
resolve(@"iOS native module
|
|
106
|
+
resolve(@"iOS native module is working!");
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
RCT_REMAP_METHOD(getModuleInfo,
|
|
@@ -111,7 +118,6 @@ RCT_REMAP_METHOD(install,
|
|
|
111
118
|
installWithResolver:(RCTPromiseResolveBlock)resolve
|
|
112
119
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
113
120
|
{
|
|
114
|
-
FaceAntiSpoofRegisterPlugin();
|
|
115
121
|
// Attempt to initialize shared model resources via FaceAntiSpoofManager
|
|
116
122
|
BOOL managerInitialized = NO;
|
|
117
123
|
Class managerClass = NSClassFromString(@"FaceAntiSpoofManager");
|
|
@@ -123,41 +129,22 @@ RCT_REMAP_METHOD(install,
|
|
|
123
129
|
}
|
|
124
130
|
}
|
|
125
131
|
|
|
126
|
-
BOOL pluginAvailable = (NSClassFromString(@"
|
|
127
|
-
NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
132
|
+
BOOL pluginAvailable = (NSClassFromString(@"FaceAntiSpoofManager") != Nil);
|
|
133
|
+
NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"FaceAntiSpoofing" ofType:@"tflite" inDirectory:@"lib/assets"];
|
|
128
134
|
if (!modelPath) {
|
|
129
|
-
|
|
130
|
-
if (fpsClass != Nil) {
|
|
131
|
-
NSBundle *podBundle = [NSBundle bundleForClass:fpsClass];
|
|
132
|
-
modelPath = [podBundle pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
133
|
-
}
|
|
135
|
+
modelPath = [[NSBundle mainBundle] pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
134
136
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
@try {
|
|
144
|
-
runtimePtr = [_faceAntiSpoofBridge valueForKeyPath:@"jsContextRef"];
|
|
145
|
-
} @catch (NSException *_) {}
|
|
146
|
-
}
|
|
147
|
-
if (!runtimePtr) {
|
|
148
|
-
@try {
|
|
149
|
-
runtimePtr = [_faceAntiSpoofBridge valueForKeyPath:@"javaScriptContextRef"];
|
|
150
|
-
} @catch (NSException *_) {}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (runtimePtr) {
|
|
154
|
-
// Pass the pointer to the JSI install stub
|
|
155
|
-
void *ptr = (__bridge void *)(runtimePtr);
|
|
156
|
-
extern void FaceAntiSpoofJSI_install(void *runtimePtr);
|
|
157
|
-
FaceAntiSpoofJSI_install(ptr);
|
|
137
|
+
if (!modelPath) {
|
|
138
|
+
Class managerClass = NSClassFromString(@"FaceAntiSpoofManager");
|
|
139
|
+
if (managerClass != Nil) {
|
|
140
|
+
NSBundle *podBundle = [NSBundle bundleForClass:managerClass];
|
|
141
|
+
modelPath = [podBundle pathForResource:@"FaceAntiSpoofing" ofType:@"tflite" inDirectory:@"lib/assets"];
|
|
142
|
+
if (!modelPath) {
|
|
143
|
+
modelPath = [podBundle pathForResource:@"FaceAntiSpoofing" ofType:@"tflite"];
|
|
144
|
+
}
|
|
158
145
|
}
|
|
159
146
|
}
|
|
160
|
-
|
|
147
|
+
BOOL modelLoaded = (modelPath != nil) || managerInitialized;
|
|
161
148
|
resolve(@(pluginAvailable && modelLoaded));
|
|
162
149
|
}
|
|
163
150
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import NitroModules
|
|
2
|
+
import VisionCamera
|
|
3
|
+
|
|
4
|
+
final class HybridSpoofDetectorFactory: HybridSpoofDetectorFactorySpec {
|
|
5
|
+
func createSpoofDetectorOutput(options: SpoofDetectorOutputOptions) throws
|
|
6
|
+
-> any HybridCameraOutputSpec
|
|
7
|
+
{
|
|
8
|
+
return HybridSpoofDetectorOutput(options: options)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import CoreImage
|
|
3
|
+
import Foundation
|
|
4
|
+
import NitroModules
|
|
5
|
+
import VisionCamera
|
|
6
|
+
|
|
7
|
+
final class HybridSpoofDetectorOutput: HybridCameraOutputSpec, NativeCameraOutput {
|
|
8
|
+
private let queue = DispatchQueue(label: "com.faceantispoof.output", qos: .userInitiated)
|
|
9
|
+
private let options: SpoofDetectorOutputOptions
|
|
10
|
+
private let delegate: SpoofDetectorDelegate
|
|
11
|
+
private let ciContext = CIContext(options: nil)
|
|
12
|
+
private let stateLock = NSLock()
|
|
13
|
+
private let targetSize = 256
|
|
14
|
+
private var isBusy = false
|
|
15
|
+
private var isDisposed = false
|
|
16
|
+
|
|
17
|
+
let output = AVCaptureVideoDataOutput()
|
|
18
|
+
let requiresAudioInput = false
|
|
19
|
+
let requiresDepthFormat = false
|
|
20
|
+
let mediaType: MediaType = .video
|
|
21
|
+
var streamType: StreamType = .video
|
|
22
|
+
var outputOrientation: CameraOrientation = .up
|
|
23
|
+
|
|
24
|
+
var currentResolution: margelo.nitro.camera.Size? {
|
|
25
|
+
guard let connection = output.connection(with: .video) else { return nil }
|
|
26
|
+
return connection.inputStreamResolution
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var targetResolution: ResolutionRule {
|
|
30
|
+
return .closestTo(margelo.nitro.camera.Size(width: 720.0, height: 1280.0))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
init(options: SpoofDetectorOutputOptions) {
|
|
34
|
+
self.options = options
|
|
35
|
+
self.delegate = SpoofDetectorDelegate()
|
|
36
|
+
super.init()
|
|
37
|
+
|
|
38
|
+
delegate.onSampleBuffer = { [weak self] sampleBuffer in
|
|
39
|
+
self?.process(sampleBuffer)
|
|
40
|
+
}
|
|
41
|
+
output.setSampleBufferDelegate(delegate, queue: queue)
|
|
42
|
+
output.alwaysDiscardsLateVideoFrames = true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func configure(config: OutputConfiguration) {
|
|
46
|
+
guard let connection = output.connection(with: .video) else { return }
|
|
47
|
+
connection.preferredVideoStabilizationMode = .off
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func dispose() {
|
|
51
|
+
stateLock.lock()
|
|
52
|
+
isDisposed = true
|
|
53
|
+
stateLock.unlock()
|
|
54
|
+
output.setSampleBufferDelegate(nil, queue: nil)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private func process(_ sampleBuffer: CMSampleBuffer) {
|
|
58
|
+
stateLock.lock()
|
|
59
|
+
guard !isDisposed, !isBusy else {
|
|
60
|
+
stateLock.unlock()
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
isBusy = true
|
|
64
|
+
stateLock.unlock()
|
|
65
|
+
|
|
66
|
+
guard let input = makeRGBInput(from: sampleBuffer) else {
|
|
67
|
+
stateLock.lock()
|
|
68
|
+
isBusy = false
|
|
69
|
+
stateLock.unlock()
|
|
70
|
+
options.onError?(RuntimeError.error(withMessage: "Failed to create RGB frame input."))
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
queue.async { [weak self] in
|
|
75
|
+
guard let self else { return }
|
|
76
|
+
defer {
|
|
77
|
+
self.stateLock.lock()
|
|
78
|
+
self.isBusy = false
|
|
79
|
+
self.stateLock.unlock()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
do {
|
|
83
|
+
let result = try FaceAntiSpoofManager.process(input: input)
|
|
84
|
+
self.options.onResult(
|
|
85
|
+
AntiSpoofingResult(
|
|
86
|
+
isLive: result.isLive,
|
|
87
|
+
label: result.isLive ? "Live Face" : "Spoof Face",
|
|
88
|
+
neuralNetworkScore: result.neuralNetworkScore,
|
|
89
|
+
laplacianScore: result.laplacianScore,
|
|
90
|
+
combinedScore: result.combinedScore,
|
|
91
|
+
confidence: result.confidence
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
} catch {
|
|
95
|
+
self.options.onError?(error)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private func makeRGBInput(from sampleBuffer: CMSampleBuffer) -> [Float]? {
|
|
101
|
+
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return nil }
|
|
102
|
+
|
|
103
|
+
let image = CIImage(cvPixelBuffer: pixelBuffer)
|
|
104
|
+
let oriented = image.oriented(forExifOrientation: outputOrientation.exifOrientation)
|
|
105
|
+
guard let cgImage = ciContext.createCGImage(oriented, from: oriented.extent) else {
|
|
106
|
+
return nil
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let width = targetSize
|
|
110
|
+
let height = targetSize
|
|
111
|
+
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
|
112
|
+
var bytes = [UInt8](repeating: 0, count: width * height * 4)
|
|
113
|
+
let rendered = bytes.withUnsafeMutableBytes { rawBuffer -> Bool in
|
|
114
|
+
guard let baseAddress = rawBuffer.baseAddress,
|
|
115
|
+
let context = CGContext(
|
|
116
|
+
data: baseAddress,
|
|
117
|
+
width: width,
|
|
118
|
+
height: height,
|
|
119
|
+
bitsPerComponent: 8,
|
|
120
|
+
bytesPerRow: width * 4,
|
|
121
|
+
space: colorSpace,
|
|
122
|
+
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
|
123
|
+
) else { return false }
|
|
124
|
+
context.interpolationQuality = .high
|
|
125
|
+
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
guard rendered else { return nil }
|
|
129
|
+
|
|
130
|
+
var result = [Float](repeating: 0, count: width * height * 3)
|
|
131
|
+
for index in 0..<(width * height) {
|
|
132
|
+
let source = index * 4
|
|
133
|
+
let destination = index * 3
|
|
134
|
+
result[destination] = Float(bytes[source]) / 255.0
|
|
135
|
+
result[destination + 1] = Float(bytes[source + 1]) / 255.0
|
|
136
|
+
result[destination + 2] = Float(bytes[source + 2]) / 255.0
|
|
137
|
+
}
|
|
138
|
+
return result
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private final class SpoofDetectorDelegate: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
|
|
143
|
+
var onSampleBuffer: ((CMSampleBuffer) -> Void)?
|
|
144
|
+
|
|
145
|
+
func captureOutput(
|
|
146
|
+
_ output: AVCaptureOutput,
|
|
147
|
+
didOutput sampleBuffer: CMSampleBuffer,
|
|
148
|
+
from connection: AVCaptureConnection
|
|
149
|
+
) {
|
|
150
|
+
onSampleBuffer?(sampleBuffer)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private extension CameraOrientation {
|
|
155
|
+
var exifOrientation: Int32 {
|
|
156
|
+
switch self {
|
|
157
|
+
case .up: return 1
|
|
158
|
+
case .right: return 6
|
|
159
|
+
case .down: return 3
|
|
160
|
+
case .left: return 8
|
|
161
|
+
@unknown default: return 1
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
package/nitro.json
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
},
|
|
11
11
|
"autolinking": {
|
|
12
12
|
"SpoofDetectorFactory": {
|
|
13
|
+
"ios": {
|
|
14
|
+
"language": "swift",
|
|
15
|
+
"implementationClassName": "HybridSpoofDetectorFactory"
|
|
16
|
+
},
|
|
13
17
|
"android": {
|
|
14
18
|
"language": "kotlin",
|
|
15
19
|
"implementationClassName": "HybridSpoofDetectorFactory"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#
|
|
2
|
+
# VisionCameraSpoofDetector+autolinking.rb
|
|
3
|
+
# This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
|
4
|
+
# https://github.com/mrousavy/nitro
|
|
5
|
+
# Copyright © Marc Rousavy @ Margelo
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
# This is a Ruby script that adds all files generated by Nitrogen
|
|
9
|
+
# to the given podspec.
|
|
10
|
+
#
|
|
11
|
+
# To use it, add this to your .podspec:
|
|
12
|
+
# ```ruby
|
|
13
|
+
# Pod::Spec.new do |spec|
|
|
14
|
+
# # ...
|
|
15
|
+
#
|
|
16
|
+
# # Add all files generated by Nitrogen
|
|
17
|
+
# load 'nitrogen/generated/ios/VisionCameraSpoofDetector+autolinking.rb'
|
|
18
|
+
# add_nitrogen_files(spec)
|
|
19
|
+
# end
|
|
20
|
+
# ```
|
|
21
|
+
|
|
22
|
+
def add_nitrogen_files(spec)
|
|
23
|
+
Pod::UI.puts "[NitroModules] 🔥 VisionCameraSpoofDetector is boosted by nitro!"
|
|
24
|
+
|
|
25
|
+
spec.dependency "NitroModules"
|
|
26
|
+
|
|
27
|
+
current_source_files = Array(spec.attributes_hash['source_files'])
|
|
28
|
+
spec.source_files = current_source_files + [
|
|
29
|
+
# Generated cross-platform specs
|
|
30
|
+
"nitrogen/generated/shared/**/*.{h,hpp,c,cpp,swift}",
|
|
31
|
+
# Generated bridges for the cross-platform specs
|
|
32
|
+
"nitrogen/generated/ios/**/*.{h,hpp,c,cpp,mm,swift}",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
current_public_header_files = Array(spec.attributes_hash['public_header_files'])
|
|
36
|
+
spec.public_header_files = current_public_header_files + [
|
|
37
|
+
# Generated specs
|
|
38
|
+
"nitrogen/generated/shared/**/*.{h,hpp}",
|
|
39
|
+
# Swift to C++ bridging helpers
|
|
40
|
+
"nitrogen/generated/ios/VisionCameraSpoofDetector-Swift-Cxx-Bridge.hpp"
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
current_private_header_files = Array(spec.attributes_hash['private_header_files'])
|
|
44
|
+
spec.private_header_files = current_private_header_files + [
|
|
45
|
+
# iOS specific specs
|
|
46
|
+
"nitrogen/generated/ios/c++/**/*.{h,hpp}",
|
|
47
|
+
# Views are framework-specific and should be private
|
|
48
|
+
"nitrogen/generated/shared/**/views/**/*"
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
current_pod_target_xcconfig = spec.attributes_hash['pod_target_xcconfig'] || {}
|
|
52
|
+
spec.pod_target_xcconfig = current_pod_target_xcconfig.merge({
|
|
53
|
+
# Use C++ 20
|
|
54
|
+
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
|
|
55
|
+
# Enables C++ <-> Swift interop (by default it's only ObjC)
|
|
56
|
+
"SWIFT_OBJC_INTEROP_MODE" => "objcxx",
|
|
57
|
+
# Enables stricter modular headers
|
|
58
|
+
"DEFINES_MODULE" => "YES",
|
|
59
|
+
# Disable auto-generated ObjC header for Swift (Static linkage on Xcode 26.4 breaks here)
|
|
60
|
+
"SWIFT_INSTALL_OBJC_HEADER" => "NO",
|
|
61
|
+
})
|
|
62
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
///
|
|
2
|
+
/// VisionCameraSpoofDetector-Swift-Cxx-Bridge.cpp
|
|
3
|
+
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
|
4
|
+
/// https://github.com/mrousavy/nitro
|
|
5
|
+
/// Copyright © Marc Rousavy @ Margelo
|
|
6
|
+
///
|
|
7
|
+
|
|
8
|
+
#include "VisionCameraSpoofDetector-Swift-Cxx-Bridge.hpp"
|
|
9
|
+
|
|
10
|
+
// Include C++ implementation defined types
|
|
11
|
+
#include "HybridSpoofDetectorFactorySpecSwift.hpp"
|
|
12
|
+
#include "VisionCameraSpoofDetector-Swift-Cxx-Umbrella.hpp"
|
|
13
|
+
#include <NitroModules/NitroDefines.hpp>
|
|
14
|
+
#include <VisionCamera/VisionCamera-Swift-Cxx-Bridge.hpp>
|
|
15
|
+
|
|
16
|
+
namespace margelo::nitro::camera::spoofdetector::bridge::swift {
|
|
17
|
+
|
|
18
|
+
// pragma MARK: std::shared_ptr<margelo::nitro::camera::HybridCameraOutputSpec>
|
|
19
|
+
std::shared_ptr<margelo::nitro::camera::HybridCameraOutputSpec> create_std__shared_ptr_margelo__nitro__camera__HybridCameraOutputSpec_(void* NON_NULL swiftUnsafePointer) noexcept {
|
|
20
|
+
// Implemented in VisionCamera
|
|
21
|
+
return margelo::nitro::camera::bridge::swift::create_std__shared_ptr_HybridCameraOutputSpec_(swiftUnsafePointer);
|
|
22
|
+
}
|
|
23
|
+
void* NON_NULL get_std__shared_ptr_margelo__nitro__camera__HybridCameraOutputSpec_(std__shared_ptr_margelo__nitro__camera__HybridCameraOutputSpec_ cppType) {
|
|
24
|
+
// Implemented in VisionCamera
|
|
25
|
+
return margelo::nitro::camera::bridge::swift::get_std__shared_ptr_HybridCameraOutputSpec_(cppType);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// pragma MARK: std::function<void(const AntiSpoofingResult& /* result */)>
|
|
29
|
+
Func_void_AntiSpoofingResult create_Func_void_AntiSpoofingResult(void* NON_NULL swiftClosureWrapper) noexcept {
|
|
30
|
+
auto swiftClosure = VisionCameraSpoofDetector::Func_void_AntiSpoofingResult::fromUnsafe(swiftClosureWrapper);
|
|
31
|
+
return [swiftClosure = std::move(swiftClosure)](const AntiSpoofingResult& result) mutable -> void {
|
|
32
|
+
swiftClosure.call(result);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// pragma MARK: std::function<void(const std::exception_ptr& /* error */)>
|
|
37
|
+
Func_void_std__exception_ptr create_Func_void_std__exception_ptr(void* NON_NULL swiftClosureWrapper) noexcept {
|
|
38
|
+
auto swiftClosure = VisionCameraSpoofDetector::Func_void_std__exception_ptr::fromUnsafe(swiftClosureWrapper);
|
|
39
|
+
return [swiftClosure = std::move(swiftClosure)](const std::exception_ptr& error) mutable -> void {
|
|
40
|
+
swiftClosure.call(error);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// pragma MARK: std::shared_ptr<HybridSpoofDetectorFactorySpec>
|
|
45
|
+
std::shared_ptr<HybridSpoofDetectorFactorySpec> create_std__shared_ptr_HybridSpoofDetectorFactorySpec_(void* NON_NULL swiftUnsafePointer) noexcept {
|
|
46
|
+
VisionCameraSpoofDetector::HybridSpoofDetectorFactorySpec_cxx swiftPart = VisionCameraSpoofDetector::HybridSpoofDetectorFactorySpec_cxx::fromUnsafe(swiftUnsafePointer);
|
|
47
|
+
return std::make_shared<margelo::nitro::camera::spoofdetector::HybridSpoofDetectorFactorySpecSwift>(swiftPart);
|
|
48
|
+
}
|
|
49
|
+
void* NON_NULL get_std__shared_ptr_HybridSpoofDetectorFactorySpec_(std__shared_ptr_HybridSpoofDetectorFactorySpec_ cppType) {
|
|
50
|
+
std::shared_ptr<margelo::nitro::camera::spoofdetector::HybridSpoofDetectorFactorySpecSwift> swiftWrapper = std::dynamic_pointer_cast<margelo::nitro::camera::spoofdetector::HybridSpoofDetectorFactorySpecSwift>(cppType);
|
|
51
|
+
#ifdef NITRO_DEBUG
|
|
52
|
+
if (swiftWrapper == nullptr) [[unlikely]] {
|
|
53
|
+
throw std::runtime_error("Class \"HybridSpoofDetectorFactorySpec\" is not implemented in Swift!");
|
|
54
|
+
}
|
|
55
|
+
#endif
|
|
56
|
+
VisionCameraSpoofDetector::HybridSpoofDetectorFactorySpec_cxx& swiftPart = swiftWrapper->getSwiftPart();
|
|
57
|
+
return swiftPart.toUnsafe();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
} // namespace margelo::nitro::camera::spoofdetector::bridge::swift
|