capacitor-plugin-faceantispoofing 0.0.1
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/CapacitorPluginFaceantispoofing.podspec +20 -0
- package/Package.swift +28 -0
- package/README.md +175 -0
- package/android/build.gradle +64 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/assets/FaceAntiSpoofing.tflite +0 -0
- package/android/src/main/assets/onet.tflite +0 -0
- package/android/src/main/assets/pnet.tflite +0 -0
- package/android/src/main/assets/rnet.tflite +0 -0
- package/android/src/main/java/io/github/asephermann/plugins/faceantispoofing/FaceAntiSpoofing.java +112 -0
- package/android/src/main/java/io/github/asephermann/plugins/faceantispoofing/FaceAntiSpoofingPlugin.java +178 -0
- package/android/src/main/java/io/github/asephermann/plugins/faceantispoofing/MyUtil.java +174 -0
- package/android/src/main/java/io/github/asephermann/plugins/faceantispoofing/mtcnn/Align.java +28 -0
- package/android/src/main/java/io/github/asephermann/plugins/faceantispoofing/mtcnn/Box.java +73 -0
- package/android/src/main/java/io/github/asephermann/plugins/faceantispoofing/mtcnn/MTCNN.java +268 -0
- package/android/src/main/java/io/github/asephermann/plugins/faceantispoofing/mtcnn/Utils.java +25 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +104 -0
- package/dist/esm/definitions.d.ts +22 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +5 -0
- package/dist/esm/web.js +14 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +28 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +31 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/FaceAntiSpoofingPlugin/Align.swift +41 -0
- package/ios/Sources/FaceAntiSpoofingPlugin/Box.swift +70 -0
- package/ios/Sources/FaceAntiSpoofingPlugin/FaceAntiSpoofing.swift +105 -0
- package/ios/Sources/FaceAntiSpoofingPlugin/FaceAntiSpoofing.tflite +0 -0
- package/ios/Sources/FaceAntiSpoofingPlugin/FaceAntiSpoofingPlugin.swift +166 -0
- package/ios/Sources/FaceAntiSpoofingPlugin/MTCNN.swift +407 -0
- package/ios/Sources/FaceAntiSpoofingPlugin/Tools.swift +103 -0
- package/ios/Sources/FaceAntiSpoofingPlugin/onet.tflite +0 -0
- package/ios/Sources/FaceAntiSpoofingPlugin/pnet.tflite +0 -0
- package/ios/Sources/FaceAntiSpoofingPlugin/rnet.tflite +0 -0
- package/ios/Tests/FaceAntiSpoofingPluginTests/FaceAntiSpoofingTests.swift +15 -0
- package/package.json +80 -0
|
@@ -0,0 +1,20 @@
|
|
|
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 = 'CapacitorPluginFaceantispoofing'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.license = package['license']
|
|
10
|
+
s.homepage = package['repository']['url']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
|
|
13
|
+
s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
|
|
14
|
+
s.resource_bundles = {'CapacitorPluginFaceantispoofing' => ['ios/Sources/**/*.tflite']}
|
|
15
|
+
s.ios.deployment_target = '15.0'
|
|
16
|
+
s.dependency 'Capacitor'
|
|
17
|
+
s.dependency 'TensorFlowLiteSwift', '~> 2.17.0'
|
|
18
|
+
s.dependency 'TensorFlowLiteC', '~> 2.17.0'
|
|
19
|
+
s.swift_version = '5.1'
|
|
20
|
+
end
|
package/Package.swift
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// swift-tools-version: 5.9
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
let package = Package(
|
|
5
|
+
name: "CapacitorPluginFaceantispoofing",
|
|
6
|
+
platforms: [.iOS(.v15)],
|
|
7
|
+
products: [
|
|
8
|
+
.library(
|
|
9
|
+
name: "CapacitorPluginFaceantispoofing",
|
|
10
|
+
targets: ["FaceAntiSpoofingPlugin"])
|
|
11
|
+
],
|
|
12
|
+
dependencies: [
|
|
13
|
+
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
|
|
14
|
+
],
|
|
15
|
+
targets: [
|
|
16
|
+
.target(
|
|
17
|
+
name: "FaceAntiSpoofingPlugin",
|
|
18
|
+
dependencies: [
|
|
19
|
+
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
|
20
|
+
.product(name: "Cordova", package: "capacitor-swift-pm")
|
|
21
|
+
],
|
|
22
|
+
path: "ios/Sources/FaceAntiSpoofingPlugin"),
|
|
23
|
+
.testTarget(
|
|
24
|
+
name: "FaceAntiSpoofingPluginTests",
|
|
25
|
+
dependencies: ["FaceAntiSpoofingPlugin"],
|
|
26
|
+
path: "ios/Tests/FaceAntiSpoofingPluginTests")
|
|
27
|
+
]
|
|
28
|
+
)
|
package/README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# capacitor-plugin-faceantispoofing
|
|
2
|
+
|
|
3
|
+
Capacitor plugin for Passive Liveness Detection and Face Anti-Spoofing using TensorFlow Lite models.
|
|
4
|
+
|
|
5
|
+
This plugin has been migrated from [cordova-plugin-facespoofing](https://github.com/rakaraka029/cordova-plugin-facespoofing).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Face Detection**: Uses MTCNN (Multi-task Cascaded Convolutional Networks) for accurate face detection
|
|
10
|
+
- **Anti-Spoofing Detection**: Passive liveness detection using TensorFlow Lite models
|
|
11
|
+
- **Blur Detection**: Laplacian variance-based image quality assessment
|
|
12
|
+
- **Cross-Platform**: Support for both Android and iOS
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
### Using npm
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install capacitor-plugin-faceantispoofing
|
|
20
|
+
npx cap sync
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Using yarn
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
yarn add capacitor-plugin-faceantispoofing
|
|
27
|
+
npx cap sync
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { FaceAntiSpoofing } from 'capacitor-plugin-faceantispoofing';
|
|
34
|
+
|
|
35
|
+
// Detect face liveness from base64 image
|
|
36
|
+
const detectLiveness = async (base64Image: string) => {
|
|
37
|
+
try {
|
|
38
|
+
const result = await FaceAntiSpoofing.detect({
|
|
39
|
+
image: base64Image // Base64 encoded image (without data:image/...;base64, prefix)
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
console.log('Error:', result.error);
|
|
43
|
+
console.log('Liveness:', result.liveness);
|
|
44
|
+
console.log('Score:', result.score);
|
|
45
|
+
console.log('Threshold:', result.threshold);
|
|
46
|
+
console.log('Message:', result.message);
|
|
47
|
+
|
|
48
|
+
if (result.liveness) {
|
|
49
|
+
console.log('Real face detected!');
|
|
50
|
+
} else {
|
|
51
|
+
console.log('Spoof detected or no face found!');
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error('Detection failed:', error);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## API
|
|
60
|
+
|
|
61
|
+
### `detect(options)`
|
|
62
|
+
|
|
63
|
+
Detects face liveness and performs anti-spoofing detection on the provided image.
|
|
64
|
+
|
|
65
|
+
| Param | Type | Description |
|
|
66
|
+
| ------------ | ----------------------------- | -------------------------------------------------- |
|
|
67
|
+
| **options** | `<DetectOptions>` | Options for the detection |
|
|
68
|
+
| **image** | `<string>` | Base64 encoded image data or file URI |
|
|
69
|
+
|
|
70
|
+
**Returns:** `Promise<DetectionResult>`
|
|
71
|
+
|
|
72
|
+
### DetectOptions
|
|
73
|
+
|
|
74
|
+
| Prop | Type | Description |
|
|
75
|
+
| ------- | ------------------- | ---------------------------------------- |
|
|
76
|
+
| image | `string` | Base64 encoded image (without `data:image/...;base64,` prefix) or file URI starting with `file://` |
|
|
77
|
+
|
|
78
|
+
### DetectionResult
|
|
79
|
+
|
|
80
|
+
| Prop | Type | Description |
|
|
81
|
+
| --------- | ------------------- | ---------------------------------------- |
|
|
82
|
+
| error | `boolean` | Whether an error occurred |
|
|
83
|
+
| liveness | `boolean` | Whether the face is detected as real |
|
|
84
|
+
| score | `string` | The liveness score |
|
|
85
|
+
| threshold | `string` | The threshold used for classification |
|
|
86
|
+
| message | `string` | Additional information message |
|
|
87
|
+
|
|
88
|
+
## Detection Flow
|
|
89
|
+
|
|
90
|
+
1. **Face Detection**: Uses MTCNN (P-Net → R-Net → O-Net) to detect faces in the image
|
|
91
|
+
2. **Face Alignment**: Aligns the detected face based on facial landmarks
|
|
92
|
+
3. **Blur Detection**: Checks image clarity using Laplacian variance
|
|
93
|
+
4. **Anti-Spoofing**: If image is clear enough, runs the anti-spoofing model to classify as real or fake
|
|
94
|
+
|
|
95
|
+
## Response Values
|
|
96
|
+
|
|
97
|
+
- **No faces detected**: `error: true`, `message: "No faces detected"`
|
|
98
|
+
- **Image too blurry**: `error: false`, `liveness: false`, score contains Laplacian value
|
|
99
|
+
- **Real face detected**: `error: false`, `liveness: true`, score < threshold
|
|
100
|
+
- **Spoof detected**: `error: false`, `liveness: false`, score >= threshold
|
|
101
|
+
|
|
102
|
+
## Platform Support
|
|
103
|
+
|
|
104
|
+
| Platform | Supported |
|
|
105
|
+
| -------- | --------- |
|
|
106
|
+
| Android | ✅ Yes |
|
|
107
|
+
| iOS | ✅ Yes |
|
|
108
|
+
| Web | ❌ No* |
|
|
109
|
+
|
|
110
|
+
*Web is not supported as native TensorFlow Lite is required.
|
|
111
|
+
|
|
112
|
+
## Dependencies
|
|
113
|
+
|
|
114
|
+
### Android
|
|
115
|
+
- Google AI Edge LITErt (TensorFlow Lite) 1.4.0
|
|
116
|
+
- minSdkVersion: 24
|
|
117
|
+
- compileSdkVersion: 36
|
|
118
|
+
|
|
119
|
+
### iOS
|
|
120
|
+
- TensorFlowLiteSwift ~ 2.17.0
|
|
121
|
+
- TensorFlowLiteC ~ 2.17.0
|
|
122
|
+
- iOS Deployment Target: 15.0
|
|
123
|
+
|
|
124
|
+
## Model Files
|
|
125
|
+
|
|
126
|
+
This plugin includes the following TensorFlow Lite models:
|
|
127
|
+
|
|
128
|
+
- `FaceAntiSpoofing.tflite` - Anti-spoofing classification model
|
|
129
|
+
- `pnet.tflite` - MTCNN Proposal Network
|
|
130
|
+
- `rnet.tflite` - MTCNN Refine Network
|
|
131
|
+
- `onet.tflite` - MTCNN Output Network
|
|
132
|
+
|
|
133
|
+
## Threshold Values
|
|
134
|
+
|
|
135
|
+
- **Android**: THRESHOLD = 0.5, LAPLACIAN_THRESHOLD = 0
|
|
136
|
+
- **iOS**: threshold = 0.5, laplacianThreshold = 0
|
|
137
|
+
|
|
138
|
+
Note: These values have been normalized from the original Cordova plugin.
|
|
139
|
+
|
|
140
|
+
## Example Image Format
|
|
141
|
+
|
|
142
|
+
The plugin accepts images in the following formats:
|
|
143
|
+
|
|
144
|
+
1. **Base64 without prefix**: `iVBORw0KGgoAAAANSUhEUgAA...`
|
|
145
|
+
2. **Base64 with data URI**: `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...`
|
|
146
|
+
3. **File URI**: `file:///path/to/image.png`
|
|
147
|
+
|
|
148
|
+
## Error Handling
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
try {
|
|
152
|
+
const result = await FaceAntiSpoofing.detect({ image: base64Data });
|
|
153
|
+
|
|
154
|
+
if (result.error) {
|
|
155
|
+
console.error('Detection error:', result.message);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (result.liveness) {
|
|
160
|
+
// Real face detected
|
|
161
|
+
} else {
|
|
162
|
+
// Spoof or no face detected
|
|
163
|
+
}
|
|
164
|
+
} catch (e) {
|
|
165
|
+
console.error('Plugin error:', e);
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
MIT
|
|
172
|
+
|
|
173
|
+
## Credits
|
|
174
|
+
|
|
175
|
+
Originally based on [cordova-plugin-facespoofing](https://github.com/rakaraka029/cordova-plugin-facespoofing) by rakaraka029.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
ext {
|
|
2
|
+
junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
|
|
3
|
+
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1'
|
|
4
|
+
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.3.0'
|
|
5
|
+
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
buildscript {
|
|
9
|
+
repositories {
|
|
10
|
+
google()
|
|
11
|
+
mavenCentral()
|
|
12
|
+
}
|
|
13
|
+
dependencies {
|
|
14
|
+
classpath 'com.android.tools.build:gradle:8.13.0'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
apply plugin: 'com.android.library'
|
|
19
|
+
|
|
20
|
+
android {
|
|
21
|
+
namespace = "io.github.asephermann.plugins.faceantispoofing"
|
|
22
|
+
compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36
|
|
23
|
+
defaultConfig {
|
|
24
|
+
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 24
|
|
25
|
+
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 36
|
|
26
|
+
versionCode 1
|
|
27
|
+
versionName "1.0"
|
|
28
|
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
29
|
+
}
|
|
30
|
+
buildTypes {
|
|
31
|
+
release {
|
|
32
|
+
minifyEnabled false
|
|
33
|
+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
lintOptions {
|
|
37
|
+
abortOnError = false
|
|
38
|
+
}
|
|
39
|
+
compileOptions {
|
|
40
|
+
sourceCompatibility JavaVersion.VERSION_21
|
|
41
|
+
targetCompatibility JavaVersion.VERSION_21
|
|
42
|
+
}
|
|
43
|
+
aaptOptions {
|
|
44
|
+
noCompress "tflite"
|
|
45
|
+
noCompress "lite"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
repositories {
|
|
51
|
+
google()
|
|
52
|
+
mavenCentral()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
dependencies {
|
|
57
|
+
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
58
|
+
implementation project(':capacitor-android')
|
|
59
|
+
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
60
|
+
testImplementation "junit:junit:$junitVersion"
|
|
61
|
+
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
62
|
+
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
63
|
+
implementation 'com.google.ai.edge.litert:litert:1.4.0'
|
|
64
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/android/src/main/java/io/github/asephermann/plugins/faceantispoofing/FaceAntiSpoofing.java
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
package io.github.asephermann.plugins.faceantispoofing;
|
|
2
|
+
|
|
3
|
+
import android.content.res.AssetManager;
|
|
4
|
+
import android.graphics.Bitmap;
|
|
5
|
+
import android.util.Log;
|
|
6
|
+
|
|
7
|
+
import com.google.ai.edge.litert.Interpreter;
|
|
8
|
+
|
|
9
|
+
import java.io.IOException;
|
|
10
|
+
import java.util.HashMap;
|
|
11
|
+
import java.util.Map;
|
|
12
|
+
|
|
13
|
+
public class FaceAntiSpoofing {
|
|
14
|
+
private static final String MODEL_FILE = "FaceAntiSpoofing.tflite";
|
|
15
|
+
|
|
16
|
+
public static final int INPUT_IMAGE_SIZE = 256;
|
|
17
|
+
public static final float THRESHOLD = 0.5f;
|
|
18
|
+
public static final int ROUTE_INDEX = 6;
|
|
19
|
+
public static final int LAPLACE_THRESHOLD = 50;
|
|
20
|
+
public static final int LAPLACIAN_THRESHOLD = 0;
|
|
21
|
+
|
|
22
|
+
private Interpreter interpreter;
|
|
23
|
+
|
|
24
|
+
public FaceAntiSpoofing(AssetManager assetManager) throws IOException {
|
|
25
|
+
Interpreter.Options options = new Interpreter.Options();
|
|
26
|
+
options.setNumThreads(4);
|
|
27
|
+
interpreter = new Interpreter(MyUtil.loadModelFile(assetManager, MODEL_FILE), options);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public float antiSpoofing(Bitmap bitmap) {
|
|
31
|
+
Bitmap bitmapScale = Bitmap.createScaledBitmap(bitmap, INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE, true);
|
|
32
|
+
|
|
33
|
+
float[][][] img = normalizeImage(bitmapScale);
|
|
34
|
+
float[][][][] input = new float[1][][][];
|
|
35
|
+
input[0] = img;
|
|
36
|
+
float[][] clss_pred = new float[1][8];
|
|
37
|
+
float[][] leaf_node_mask = new float[1][8];
|
|
38
|
+
Map<Integer, Object> outputs = new HashMap<>();
|
|
39
|
+
outputs.put(interpreter.getOutputIndex("Identity"), clss_pred);
|
|
40
|
+
outputs.put(interpreter.getOutputIndex("Identity_1"), leaf_node_mask);
|
|
41
|
+
interpreter.runForMultipleInputsOutputs(new Object[]{input}, outputs);
|
|
42
|
+
|
|
43
|
+
Log.i("FaceAntiSpoofing", "[" + clss_pred[0][0] + ", " + clss_pred[0][1] + ", "
|
|
44
|
+
+ clss_pred[0][2] + ", " + clss_pred[0][3] + ", " + clss_pred[0][4] + ", "
|
|
45
|
+
+ clss_pred[0][5] + ", " + clss_pred[0][6] + ", " + clss_pred[0][7] + "]");
|
|
46
|
+
Log.i("FaceAntiSpoofing", "[" + leaf_node_mask[0][0] + ", " + leaf_node_mask[0][1] + ", "
|
|
47
|
+
+ leaf_node_mask[0][2] + ", " + leaf_node_mask[0][3] + ", " + leaf_node_mask[0][4] + ", "
|
|
48
|
+
+ leaf_node_mask[0][5] + ", " + leaf_node_mask[0][6] + ", " + leaf_node_mask[0][7] + "]");
|
|
49
|
+
|
|
50
|
+
return leaf_score1(clss_pred, leaf_node_mask);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private float leaf_score1(float[][] clss_pred, float[][] leaf_node_mask) {
|
|
54
|
+
float score = 0;
|
|
55
|
+
for (int i = 0; i < 8; i++) {
|
|
56
|
+
score += Math.abs(clss_pred[0][i]) * leaf_node_mask[0][i];
|
|
57
|
+
}
|
|
58
|
+
return score;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private float leaf_score2(float[][] clss_pred) {
|
|
62
|
+
return clss_pred[0][ROUTE_INDEX];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public static float[][][] normalizeImage(Bitmap bitmap) {
|
|
66
|
+
int h = bitmap.getHeight();
|
|
67
|
+
int w = bitmap.getWidth();
|
|
68
|
+
float[][][] floatValues = new float[h][w][3];
|
|
69
|
+
|
|
70
|
+
float imageStd = 255;
|
|
71
|
+
int[] pixels = new int[h * w];
|
|
72
|
+
bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, w, h);
|
|
73
|
+
for (int i = 0; i < h; i++) {
|
|
74
|
+
for (int j = 0; j < w; j++) {
|
|
75
|
+
final int val = pixels[i * w + j];
|
|
76
|
+
float r = ((val >> 16) & 0xFF) / imageStd;
|
|
77
|
+
float g = ((val >> 8) & 0xFF) / imageStd;
|
|
78
|
+
float b = (val & 0xFF) / imageStd;
|
|
79
|
+
|
|
80
|
+
float[] arr = {r, g, b};
|
|
81
|
+
floatValues[i][j] = arr;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return floatValues;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public int laplacian(Bitmap bitmap) {
|
|
88
|
+
Bitmap bitmapScale = Bitmap.createScaledBitmap(bitmap, INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE, true);
|
|
89
|
+
|
|
90
|
+
int[][] laplace = {{0, 1, 0}, {1, -4, 1}, {0, 1, 0}};
|
|
91
|
+
int size = laplace.length;
|
|
92
|
+
int[][] img = MyUtil.convertGreyImg(bitmapScale);
|
|
93
|
+
int height = img.length;
|
|
94
|
+
int width = img[0].length;
|
|
95
|
+
|
|
96
|
+
int score = 0;
|
|
97
|
+
for (int x = 0; x < height - size + 1; x++){
|
|
98
|
+
for (int y = 0; y < width - size + 1; y++){
|
|
99
|
+
int result = 0;
|
|
100
|
+
for (int i = 0; i < size; i++){
|
|
101
|
+
for (int j = 0; j < size; j++){
|
|
102
|
+
result += (img[x + i][y + j] & 0xFF) * laplace[i][j];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (result > LAPLACE_THRESHOLD) {
|
|
106
|
+
score++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return score;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
package io.github.asephermann.plugins.faceantispoofing;
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap;
|
|
4
|
+
import android.graphics.BitmapFactory;
|
|
5
|
+
import android.util.Base64;
|
|
6
|
+
import android.util.Log;
|
|
7
|
+
|
|
8
|
+
import com.getcapacitor.JSObject;
|
|
9
|
+
import com.getcapacitor.Plugin;
|
|
10
|
+
import com.getcapacitor.PluginCall;
|
|
11
|
+
import com.getcapacitor.PluginMethod;
|
|
12
|
+
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
13
|
+
|
|
14
|
+
import java.io.File;
|
|
15
|
+
import java.io.FileInputStream;
|
|
16
|
+
import java.io.IOException;
|
|
17
|
+
import java.util.Vector;
|
|
18
|
+
|
|
19
|
+
import io.github.asephermann.plugins.faceantispoofing.mtcnn.Align;
|
|
20
|
+
import io.github.asephermann.plugins.faceantispoofing.mtcnn.Box;
|
|
21
|
+
import io.github.asephermann.plugins.faceantispoofing.mtcnn.MTCNN;
|
|
22
|
+
|
|
23
|
+
@CapacitorPlugin(name = "FaceAntiSpoofing")
|
|
24
|
+
public class FaceAntiSpoofingPlugin extends Plugin {
|
|
25
|
+
|
|
26
|
+
private static final String TAG = "FaceAntiSpoofing";
|
|
27
|
+
|
|
28
|
+
private FaceAntiSpoofing faceAntiSpoofing;
|
|
29
|
+
private MTCNN mtcnn;
|
|
30
|
+
|
|
31
|
+
@Override
|
|
32
|
+
public void load() {
|
|
33
|
+
super.load();
|
|
34
|
+
try {
|
|
35
|
+
faceAntiSpoofing = new FaceAntiSpoofing(getContext().getAssets());
|
|
36
|
+
mtcnn = new MTCNN(getContext().getAssets());
|
|
37
|
+
Log.d(TAG, "Models loaded successfully");
|
|
38
|
+
} catch (IOException e) {
|
|
39
|
+
Log.e(TAG, "Failed to load models", e);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@PluginMethod
|
|
44
|
+
public void detect(PluginCall call) {
|
|
45
|
+
String image = call.getString("image");
|
|
46
|
+
|
|
47
|
+
if (image == null || image.isEmpty()) {
|
|
48
|
+
call.reject("Image data is required");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (faceAntiSpoofing == null || mtcnn == null) {
|
|
53
|
+
call.reject("Models not loaded. Please ensure the plugin is properly initialized.");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
Bitmap bitmap = decodeImage(image);
|
|
59
|
+
if (bitmap == null) {
|
|
60
|
+
call.reject("Failed to decode image");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
DetectionResult result = performAntiSpoofing(bitmap);
|
|
65
|
+
call.resolve(resultToJSObject(result));
|
|
66
|
+
} catch (Exception e) {
|
|
67
|
+
Log.e(TAG, "Error during detection", e);
|
|
68
|
+
call.reject("Error during detection: " + e.getMessage());
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private Bitmap decodeImage(String imageData) throws IOException {
|
|
73
|
+
// Check if it's a base64 string or file path
|
|
74
|
+
if (imageData.startsWith("data:image/")) {
|
|
75
|
+
// Remove data URI prefix if present
|
|
76
|
+
int commaIndex = imageData.indexOf(',');
|
|
77
|
+
if (commaIndex != -1) {
|
|
78
|
+
imageData = imageData.substring(commaIndex + 1);
|
|
79
|
+
}
|
|
80
|
+
byte[] decodedString = Base64.decode(imageData, Base64.DEFAULT);
|
|
81
|
+
return BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
|
|
82
|
+
} else if (imageData.startsWith("file://")) {
|
|
83
|
+
String filePath = imageData.substring(7);
|
|
84
|
+
File file = new File(filePath);
|
|
85
|
+
if (file.exists()) {
|
|
86
|
+
FileInputStream fis = new FileInputStream(file);
|
|
87
|
+
Bitmap bitmap = BitmapFactory.decodeStream(fis);
|
|
88
|
+
fis.close();
|
|
89
|
+
return bitmap;
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
// Try as base64
|
|
93
|
+
byte[] decodedString = Base64.decode(imageData, Base64.DEFAULT);
|
|
94
|
+
return BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private DetectionResult performAntiSpoofing(Bitmap bitmap) {
|
|
100
|
+
DetectionResult result = new DetectionResult();
|
|
101
|
+
|
|
102
|
+
if (bitmap == null) {
|
|
103
|
+
result.error = true;
|
|
104
|
+
result.message = "Please detect face first";
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
Bitmap bitmapTemp1 = bitmap.copy(bitmap.getConfig(), false);
|
|
109
|
+
Vector<Box> boxes1 = mtcnn.detectFaces(bitmapTemp1, bitmapTemp1.getWidth() / 8);
|
|
110
|
+
|
|
111
|
+
if (boxes1.size() == 0) {
|
|
112
|
+
result.error = true;
|
|
113
|
+
result.message = "No faces detected";
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
Box box1 = boxes1.get(0);
|
|
118
|
+
bitmapTemp1 = Align.face_align(bitmapTemp1, box1.landmark);
|
|
119
|
+
boxes1 = mtcnn.detectFaces(bitmapTemp1, bitmapTemp1.getWidth() / 8);
|
|
120
|
+
|
|
121
|
+
if (boxes1.size() == 0) {
|
|
122
|
+
result.error = true;
|
|
123
|
+
result.message = "No faces detected";
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
box1 = boxes1.get(0);
|
|
128
|
+
box1.toSquareShape();
|
|
129
|
+
box1.limitSquare(bitmapTemp1.getWidth(), bitmapTemp1.getHeight());
|
|
130
|
+
android.graphics.Rect rect1 = box1.transform2Rect();
|
|
131
|
+
Bitmap bitmapCrop1 = MyUtil.crop(bitmapTemp1, rect1);
|
|
132
|
+
|
|
133
|
+
int laplace2 = faceAntiSpoofing.laplacian(bitmapCrop1);
|
|
134
|
+
|
|
135
|
+
if (laplace2 < FaceAntiSpoofing.LAPLACIAN_THRESHOLD) {
|
|
136
|
+
result.error = false;
|
|
137
|
+
result.score = String.valueOf(laplace2);
|
|
138
|
+
result.threshold = String.valueOf(FaceAntiSpoofing.LAPLACIAN_THRESHOLD);
|
|
139
|
+
result.liveness = false;
|
|
140
|
+
result.message = "";
|
|
141
|
+
} else {
|
|
142
|
+
float score2 = faceAntiSpoofing.antiSpoofing(bitmapCrop1);
|
|
143
|
+
if (score2 < FaceAntiSpoofing.THRESHOLD) {
|
|
144
|
+
result.error = false;
|
|
145
|
+
result.score = String.valueOf(score2);
|
|
146
|
+
result.threshold = String.valueOf(FaceAntiSpoofing.THRESHOLD);
|
|
147
|
+
result.liveness = true;
|
|
148
|
+
result.message = "";
|
|
149
|
+
} else {
|
|
150
|
+
result.error = false;
|
|
151
|
+
result.score = String.valueOf(score2);
|
|
152
|
+
result.threshold = String.valueOf(FaceAntiSpoofing.THRESHOLD);
|
|
153
|
+
result.liveness = false;
|
|
154
|
+
result.message = "Sorry, we can't find a face or indicate that the photo is real.";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private JSObject resultToJSObject(DetectionResult result) {
|
|
162
|
+
JSObject jsObject = new JSObject();
|
|
163
|
+
jsObject.put("error", result.error);
|
|
164
|
+
jsObject.put("liveness", result.liveness);
|
|
165
|
+
jsObject.put("score", result.score);
|
|
166
|
+
jsObject.put("threshold", result.threshold);
|
|
167
|
+
jsObject.put("message", result.message);
|
|
168
|
+
return jsObject;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private static class DetectionResult {
|
|
172
|
+
boolean error = false;
|
|
173
|
+
boolean liveness = false;
|
|
174
|
+
String score = "0";
|
|
175
|
+
String threshold = "0";
|
|
176
|
+
String message = "";
|
|
177
|
+
}
|
|
178
|
+
}
|