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.
Files changed (31) hide show
  1. package/README.md +103 -186
  2. package/VisionCameraSpoofDetector.podspec +41 -0
  3. package/ios/FaceAntiSpoofManager.swift +106 -4
  4. package/ios/FaceAntiSpoofModule.m +38 -51
  5. package/ios/HybridSpoofDetectorFactory.swift +10 -0
  6. package/ios/HybridSpoofDetectorOutput.swift +164 -0
  7. package/nitro.json +4 -0
  8. package/nitrogen/generated/ios/VisionCameraSpoofDetector+autolinking.rb +62 -0
  9. package/nitrogen/generated/ios/VisionCameraSpoofDetector-Swift-Cxx-Bridge.cpp +60 -0
  10. package/nitrogen/generated/ios/VisionCameraSpoofDetector-Swift-Cxx-Bridge.hpp +151 -0
  11. package/nitrogen/generated/ios/VisionCameraSpoofDetector-Swift-Cxx-Umbrella.hpp +60 -0
  12. package/nitrogen/generated/ios/VisionCameraSpoofDetectorAutolinking.mm +33 -0
  13. package/nitrogen/generated/ios/VisionCameraSpoofDetectorAutolinking.swift +26 -0
  14. package/nitrogen/generated/ios/c++/HybridSpoofDetectorFactorySpecSwift.cpp +11 -0
  15. package/nitrogen/generated/ios/c++/HybridSpoofDetectorFactorySpecSwift.hpp +97 -0
  16. package/nitrogen/generated/ios/swift/AntiSpoofingResult.swift +54 -0
  17. package/nitrogen/generated/ios/swift/Func_void_AntiSpoofingResult.swift +46 -0
  18. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  19. package/nitrogen/generated/ios/swift/HybridSpoofDetectorFactorySpec.swift +56 -0
  20. package/nitrogen/generated/ios/swift/HybridSpoofDetectorFactorySpec_cxx.swift +142 -0
  21. package/nitrogen/generated/ios/swift/Size.swift +34 -0
  22. package/nitrogen/generated/ios/swift/SpoofDetectorOutputOptions.swift +74 -0
  23. package/package.json +8 -3
  24. package/src/specs/SpoofDetectorFactory.nitro.ts +1 -0
  25. package/ios/FaceAntiSpoofFrameProcessor.swift +0 -301
  26. package/ios/FaceAntiSpoofJSI.h +0 -12
  27. package/ios/FaceAntiSpoofJSI.mm +0 -92
  28. package/ios/FaceAntiSpoofPluginRegister.m +0 -84
  29. package/ios/models/FaceAntiSpoofing.tflite +0 -0
  30. package/react-native-vision-camera-spoof-detector.podspec +0 -22
  31. /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.60.0
23
- - React Native Vision Camera `^4.6.4` or `^5.0.0`
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 (optional, for enhanced features)
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
- > **Important:** Request camera permission before rendering `Camera`, and call `initializeFaceAntiSpoof()` once before the frame processor starts. The frame processor returns `null` until native initialization or a result is available. Native inference is throttled and may return the most recent result while another frame is being processed.
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 { Camera, useCameraDevices, useCameraPermission, useFrameProcessor } from 'react-native-vision-camera';
93
- import { faceAntiSpoofFrameProcessor, initializeFaceAntiSpoof } from 'react-native-vision-camera-spoof-detector';
94
- import { runOnJS } from 'react-native-reanimated';
95
-
96
- export default function App() {
97
- const devices = useCameraDevices();
98
- const device = devices.front;
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
- useEffect(() => {
107
- initializeFaceAntiSpoof()
108
- .then((success) => console.log('FaceAntiSpoof initialized:', success))
109
- .catch((error) => console.error('FaceAntiSpoof initialization failed:', error));
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 frameProcessor = useFrameProcessor((frame) => {
113
- 'worklet';
114
- const result = faceAntiSpoofFrameProcessor(frame);
115
- if (result) {
116
- runOnJS(setSpoofResult)(result);
117
- }
118
- }, []);
125
+ const antiSpoofOutput = useFaceAntiSpoofFrameOutput({
126
+ onResult(result) {
127
+ setSpoofResult(result);
128
+ },
129
+ });
119
130
 
120
- if (!hasPermission || device == null) return <Text>Waiting for camera permission...</Text>;
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={true}
128
- frameProcessor={frameProcessor}
129
- frameProcessorFps={5}
143
+ isActive
144
+ outputs={outputs}
130
145
  />
131
- {spoofResult && (
132
- <View style={styles.resultContainer}>
133
- <Text style={styles.resultText}>
134
- Is Live: {spoofResult.isLive ? 'Yes' : 'No'}
135
- </Text>
136
- <Text style={styles.resultText}>
137
- Score: {spoofResult.neuralNetworkScore?.toFixed(2)}
138
- </Text>
139
- <Text style={styles.resultText}>
140
- Label: {spoofResult.label}
141
- </Text>
142
- </View>
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
- resultContainer: {
151
- position: 'absolute',
152
- bottom: 50,
153
- left: 0,
154
- right: 0,
171
+ messageContainer: {
172
+ flex: 1,
155
173
  alignItems: 'center',
156
- backgroundColor: 'rgba(0,0,0,0.5)',
157
- padding: 10,
174
+ justifyContent: 'center',
175
+ paddingHorizontal: 24,
158
176
  },
159
- resultText: {
160
- color: 'white',
161
- fontSize: 20,
162
- fontWeight: 'bold',
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
- ### Advanced Usage with Full Feature Set
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
- ```javascript
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
- const isMounted = useRef(true);
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
- useEffect(() => {
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
- const frameProcessor = useFrameProcessor(
241
- (frame) => {
242
- 'worklet';
243
-
244
- try {
245
- const detected = detectFaces?.(frame);
246
-
247
- if (!detected || detected.length === 0) {
248
- onFacesUpdate?.({ count: 0, progress: 0 });
249
- return;
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
- return {
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
- export default useFaceDetectionFrameProcessor;
285
- ```
214
+ **Options**: `onResult(result)` and optional `onError(error)`.
286
215
 
287
- ## 📚 API Reference
216
+ ---
288
217
 
289
218
  ### `initializeFaceAntiSpoof()`
290
219
 
291
- Initializes the face anti-spoofing module. Must be called before using the frame processor.
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(forResource: "FaceAntiSpoofing", ofType: "tflite")
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 fpsClass = NSClassFromString("FaceAntiSpoofFrameProcessor") as? AnyClass {
21
- let podBundle = Bundle(for: fpsClass)
22
- modelPath = podBundle.path(forResource: "FaceAntiSpoofing", ofType: "tflite")
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