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 @@
|
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export interface DetectionResult {\n error: boolean;\n liveness: boolean;\n score: string;\n threshold: string;\n message: string;\n}\n\nexport interface DetectOptions {\n /**\n * Base64 encoded image data (without data:image/...;base64, prefix)\n * or file URI to the image\n */\n image: string;\n}\n\nexport interface FaceAntiSpoofingPlugin {\n /**\n * Detect face liveness and anti-spoofing\n * @param options Image data as base64 string or file URI\n * @returns Promise with detection result containing liveness status\n */\n detect(options: DetectOptions): Promise<DetectionResult>;\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { registerPlugin } from '@capacitor/core';
|
|
2
|
+
const FaceAntiSpoofing = registerPlugin('FaceAntiSpoofing', {
|
|
3
|
+
web: () => import('./web').then((m) => new m.FaceAntiSpoofingWeb()),
|
|
4
|
+
});
|
|
5
|
+
export * from './definitions';
|
|
6
|
+
export { FaceAntiSpoofing };
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,gBAAgB,GAAG,cAAc,CAAyB,kBAAkB,EAAE;IAClF,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,mBAAmB,EAAE,CAAC;CACpE,CAAC,CAAC;AAEH,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,gBAAgB,EAAE,CAAC","sourcesContent":["import { registerPlugin } from '@capacitor/core';\n\nimport type { FaceAntiSpoofingPlugin } from './definitions';\n\nconst FaceAntiSpoofing = registerPlugin<FaceAntiSpoofingPlugin>('FaceAntiSpoofing', {\n web: () => import('./web').then((m) => new m.FaceAntiSpoofingWeb()),\n});\n\nexport * from './definitions';\nexport { FaceAntiSpoofing };\n"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { WebPlugin } from '@capacitor/core';
|
|
2
|
+
import type { DetectionResult, DetectOptions, FaceAntiSpoofingPlugin } from './definitions';
|
|
3
|
+
export declare class FaceAntiSpoofingWeb extends WebPlugin implements FaceAntiSpoofingPlugin {
|
|
4
|
+
detect(_options: DetectOptions): Promise<DetectionResult>;
|
|
5
|
+
}
|
package/dist/esm/web.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { WebPlugin } from '@capacitor/core';
|
|
2
|
+
export class FaceAntiSpoofingWeb extends WebPlugin {
|
|
3
|
+
async detect(_options) {
|
|
4
|
+
console.log('Face anti-spoofing is not supported on web platform');
|
|
5
|
+
return {
|
|
6
|
+
error: true,
|
|
7
|
+
liveness: false,
|
|
8
|
+
score: '0',
|
|
9
|
+
threshold: '0',
|
|
10
|
+
message: 'Face anti-spoofing is not supported on web platform. Please use a native platform (iOS or Android).',
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=web.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAI5C,MAAM,OAAO,mBAAoB,SAAQ,SAAS;IAChD,KAAK,CAAC,MAAM,CAAC,QAAuB;QAClC,OAAO,CAAC,GAAG,CAAC,qDAAqD,CAAC,CAAC;QACnE,OAAO;YACL,KAAK,EAAE,IAAI;YACX,QAAQ,EAAE,KAAK;YACf,KAAK,EAAE,GAAG;YACV,SAAS,EAAE,GAAG;YACd,OAAO,EAAE,qGAAqG;SAC/G,CAAC;IACJ,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\n\nimport type { DetectionResult, DetectOptions, FaceAntiSpoofingPlugin } from './definitions';\n\nexport class FaceAntiSpoofingWeb extends WebPlugin implements FaceAntiSpoofingPlugin {\n async detect(_options: DetectOptions): Promise<DetectionResult> {\n console.log('Face anti-spoofing is not supported on web platform');\n return {\n error: true,\n liveness: false,\n score: '0',\n threshold: '0',\n message: 'Face anti-spoofing is not supported on web platform. Please use a native platform (iOS or Android).',\n };\n }\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@capacitor/core');
|
|
4
|
+
|
|
5
|
+
const FaceAntiSpoofing = core.registerPlugin('FaceAntiSpoofing', {
|
|
6
|
+
web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.FaceAntiSpoofingWeb()),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
class FaceAntiSpoofingWeb extends core.WebPlugin {
|
|
10
|
+
async detect(_options) {
|
|
11
|
+
console.log('Face anti-spoofing is not supported on web platform');
|
|
12
|
+
return {
|
|
13
|
+
error: true,
|
|
14
|
+
liveness: false,
|
|
15
|
+
score: '0',
|
|
16
|
+
threshold: '0',
|
|
17
|
+
message: 'Face anti-spoofing is not supported on web platform. Please use a native platform (iOS or Android).',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
var web = /*#__PURE__*/Object.freeze({
|
|
23
|
+
__proto__: null,
|
|
24
|
+
FaceAntiSpoofingWeb: FaceAntiSpoofingWeb
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
exports.FaceAntiSpoofing = FaceAntiSpoofing;
|
|
28
|
+
//# sourceMappingURL=plugin.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst FaceAntiSpoofing = registerPlugin('FaceAntiSpoofing', {\n web: () => import('./web').then((m) => new m.FaceAntiSpoofingWeb()),\n});\nexport * from './definitions';\nexport { FaceAntiSpoofing };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class FaceAntiSpoofingWeb extends WebPlugin {\n async detect(_options) {\n console.log('Face anti-spoofing is not supported on web platform');\n return {\n error: true,\n liveness: false,\n score: '0',\n threshold: '0',\n message: 'Face anti-spoofing is not supported on web platform. Please use a native platform (iOS or Android).',\n };\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,gBAAgB,GAAGA,mBAAc,CAAC,kBAAkB,EAAE;AAC5D,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,mBAAmB,EAAE,CAAC;AACvE,CAAC;;ACFM,MAAM,mBAAmB,SAASC,cAAS,CAAC;AACnD,IAAI,MAAM,MAAM,CAAC,QAAQ,EAAE;AAC3B,QAAQ,OAAO,CAAC,GAAG,CAAC,qDAAqD,CAAC;AAC1E,QAAQ,OAAO;AACf,YAAY,KAAK,EAAE,IAAI;AACvB,YAAY,QAAQ,EAAE,KAAK;AAC3B,YAAY,KAAK,EAAE,GAAG;AACtB,YAAY,SAAS,EAAE,GAAG;AAC1B,YAAY,OAAO,EAAE,qGAAqG;AAC1H,SAAS;AACT,IAAI;AACJ;;;;;;;;;"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
var capacitorFaceAntiSpoofing = (function (exports, core) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const FaceAntiSpoofing = core.registerPlugin('FaceAntiSpoofing', {
|
|
5
|
+
web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.FaceAntiSpoofingWeb()),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
class FaceAntiSpoofingWeb extends core.WebPlugin {
|
|
9
|
+
async detect(_options) {
|
|
10
|
+
console.log('Face anti-spoofing is not supported on web platform');
|
|
11
|
+
return {
|
|
12
|
+
error: true,
|
|
13
|
+
liveness: false,
|
|
14
|
+
score: '0',
|
|
15
|
+
threshold: '0',
|
|
16
|
+
message: 'Face anti-spoofing is not supported on web platform. Please use a native platform (iOS or Android).',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var web = /*#__PURE__*/Object.freeze({
|
|
22
|
+
__proto__: null,
|
|
23
|
+
FaceAntiSpoofingWeb: FaceAntiSpoofingWeb
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
exports.FaceAntiSpoofing = FaceAntiSpoofing;
|
|
27
|
+
|
|
28
|
+
return exports;
|
|
29
|
+
|
|
30
|
+
})({}, capacitorExports);
|
|
31
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst FaceAntiSpoofing = registerPlugin('FaceAntiSpoofing', {\n web: () => import('./web').then((m) => new m.FaceAntiSpoofingWeb()),\n});\nexport * from './definitions';\nexport { FaceAntiSpoofing };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class FaceAntiSpoofingWeb extends WebPlugin {\n async detect(_options) {\n console.log('Face anti-spoofing is not supported on web platform');\n return {\n error: true,\n liveness: false,\n score: '0',\n threshold: '0',\n message: 'Face anti-spoofing is not supported on web platform. Please use a native platform (iOS or Android).',\n };\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,gBAAgB,GAAGA,mBAAc,CAAC,kBAAkB,EAAE;IAC5D,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,mBAAmB,EAAE,CAAC;IACvE,CAAC;;ICFM,MAAM,mBAAmB,SAASC,cAAS,CAAC;IACnD,IAAI,MAAM,MAAM,CAAC,QAAQ,EAAE;IAC3B,QAAQ,OAAO,CAAC,GAAG,CAAC,qDAAqD,CAAC;IAC1E,QAAQ,OAAO;IACf,YAAY,KAAK,EAAE,IAAI;IACvB,YAAY,QAAQ,EAAE,KAAK;IAC3B,YAAY,KAAK,EAAE,GAAG;IACtB,YAAY,SAAS,EAAE,GAAG;IAC1B,YAAY,OAAO,EAAE,qGAAqG;IAC1H,SAAS;IACT,IAAI;IACJ;;;;;;;;;;;;;;;"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
public class Align {
|
|
5
|
+
public static func faceAlign(image: UIImage, landmark: [CGPoint]) -> UIImage {
|
|
6
|
+
let xLeftEye = landmark[0].x
|
|
7
|
+
let yLeftEye = landmark[0].y
|
|
8
|
+
let xRightEye = landmark[1].x
|
|
9
|
+
let yRightEye = landmark[1].y
|
|
10
|
+
|
|
11
|
+
let dx = xRightEye - xLeftEye
|
|
12
|
+
let dy = yRightEye - yLeftEye
|
|
13
|
+
let angle = atan2(dy, dx) * 180.0 / .pi
|
|
14
|
+
|
|
15
|
+
let rotatedImage = image.rotated(by: -angle)
|
|
16
|
+
|
|
17
|
+
return rotatedImage
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
extension UIImage {
|
|
22
|
+
func rotated(by degrees: CGFloat) -> UIImage {
|
|
23
|
+
let radians = degrees * .pi / 180.0
|
|
24
|
+
|
|
25
|
+
let rotatedSize = CGRect(origin: .zero, size: size)
|
|
26
|
+
.applying(CGAffineTransform(rotationAngle: radians))
|
|
27
|
+
.integral.size
|
|
28
|
+
|
|
29
|
+
UIGraphicsBeginImageContext(rotatedSize)
|
|
30
|
+
defer { UIGraphicsEndImageContext() }
|
|
31
|
+
|
|
32
|
+
if let context = UIGraphicsGetCurrentContext() {
|
|
33
|
+
let origin = CGPoint(x: rotatedSize.width / 2.0, y: rotatedSize.height / 2.0)
|
|
34
|
+
context.translateBy(x: origin.x, y: origin.y)
|
|
35
|
+
context.rotate(by: radians)
|
|
36
|
+
draw(in: CGRect(x: -size.width / 2.0, y: -size.height / 2.0, width: size.width, height: size.height))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return UIGraphicsGetImageFromCurrentImageContext() ?? self
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
public class Box {
|
|
5
|
+
public var box = [Int](repeating: 0, count: 4)
|
|
6
|
+
public var score: Float = 0
|
|
7
|
+
public var bbr = [Float](repeating: 0, count: 4)
|
|
8
|
+
public var deleted = false
|
|
9
|
+
public var landmark = [CGPoint](repeating: CGPoint.zero, count: 5)
|
|
10
|
+
|
|
11
|
+
public func left() -> Int {
|
|
12
|
+
return box[0]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public func right() -> Int {
|
|
16
|
+
return box[2]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public func top() -> Int {
|
|
20
|
+
return box[1]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public func bottom() -> Int {
|
|
24
|
+
return box[3]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public func width() -> Int {
|
|
28
|
+
return box[2] - box[0] + 1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public func height() -> Int {
|
|
32
|
+
return box[3] - box[1] + 1
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public func transform2Rect() -> CGRect {
|
|
36
|
+
return CGRect(x: box[0], y: box[1], width: width(), height: height())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public func area() -> Int {
|
|
40
|
+
return width() * height()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public func calibrate() {
|
|
44
|
+
let w = width()
|
|
45
|
+
let h = height()
|
|
46
|
+
box[0] = Int(round(Double(box[0] + Int(bbr[0] * Float(w)))))
|
|
47
|
+
box[1] = Int(round(Double(box[1] + Int(bbr[1] * Float(h)))))
|
|
48
|
+
box[2] = Int(round(Double(box[2] + Int(bbr[2] * Float(w)))))
|
|
49
|
+
box[3] = Int(round(Double(box[3] + Int(bbr[3] * Float(h)))))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public func toSquareShape() {
|
|
53
|
+
let w = width()
|
|
54
|
+
let h = height()
|
|
55
|
+
let maxSide = max(w, h)
|
|
56
|
+
box[2] = box[0] + maxSide - 1
|
|
57
|
+
box[3] = box[1] + maxSide - 1
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public func limitSquare(w: Int, h: Int) {
|
|
61
|
+
if box[0] < 0 { box[0] = 0 }
|
|
62
|
+
if box[1] < 0 { box[1] = 0 }
|
|
63
|
+
if box[2] >= w { box[2] = w - 1 }
|
|
64
|
+
if box[3] >= h { box[3] = h - 1 }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public func transbound(w: Int, h: Int) -> Bool {
|
|
68
|
+
return box[0] < 0 || box[1] < 0 || box[2] >= w || box[3] >= h
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
import TensorFlowLite
|
|
4
|
+
|
|
5
|
+
public class FaceAntiSpoofing {
|
|
6
|
+
private static let modelFileName = "FaceAntiSpoofing"
|
|
7
|
+
private static let modelFileType = "tflite"
|
|
8
|
+
private static let imageWidth = 256
|
|
9
|
+
private static let imageHeight = 256
|
|
10
|
+
private static let laplaceThreshold = 50
|
|
11
|
+
|
|
12
|
+
public static let threshold: Float = 0.5
|
|
13
|
+
public static let laplacianThreshold = 0
|
|
14
|
+
|
|
15
|
+
private var interpreter: Interpreter?
|
|
16
|
+
|
|
17
|
+
public init() throws {
|
|
18
|
+
let modelPath = Tools.filePathForResourceName(name: modelFileName, extension: modelFileType)
|
|
19
|
+
var options = InterpreterOptions()
|
|
20
|
+
options.numberOfThreads = 4
|
|
21
|
+
interpreter = try Interpreter(modelPath: modelPath, options: options)
|
|
22
|
+
try interpreter?.allocateTensors()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public func antiSpoofing(image: UIImage) -> Float {
|
|
26
|
+
guard let interpreter = interpreter else {
|
|
27
|
+
return 0
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let size = CGSize(width: CGFloat(Self.imageWidth), height: CGFloat(Self.imageHeight))
|
|
31
|
+
guard let imageScale = Tools.scaleImage(image: image, toSize: size),
|
|
32
|
+
let data = processData(image: imageScale) else {
|
|
33
|
+
return 0
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
do {
|
|
37
|
+
try interpreter.copy(data, toInputAt: 0)
|
|
38
|
+
try interpreter.invoke()
|
|
39
|
+
|
|
40
|
+
let clss_pred = try interpreter.output(at: 0) as [Float]
|
|
41
|
+
let leaf_node_mask = try interpreter.output(at: 1) as [Float]
|
|
42
|
+
|
|
43
|
+
var score: Float = 0
|
|
44
|
+
for i in 0..<8 {
|
|
45
|
+
score += abs(clss_pred[i]) * leaf_node_mask[i]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return score
|
|
49
|
+
} catch {
|
|
50
|
+
print("Error running inference: \(error)")
|
|
51
|
+
return 0
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private func processData(image: UIImage) -> Data? {
|
|
56
|
+
guard let image_data = Tools.convertUIImageToBitmapRGBA8(image: image) else {
|
|
57
|
+
return nil
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let input_std: Float = 255.0
|
|
61
|
+
var floats = [Float](repeating: 0, count: Self.imageWidth * Self.imageHeight * 3)
|
|
62
|
+
|
|
63
|
+
var k = 0
|
|
64
|
+
let size = Self.imageWidth * Self.imageHeight * 4
|
|
65
|
+
for j in 0..<size {
|
|
66
|
+
if j % 4 == 3 {
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
floats[k] = Float(image_data[j]) / input_std
|
|
70
|
+
k += 1
|
|
71
|
+
}
|
|
72
|
+
image_data.deallocate()
|
|
73
|
+
|
|
74
|
+
return Data(bytes: floats, count: MemoryLayout<Float>.stride * floats.count)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public func laplacian(image: UIImage) -> Int {
|
|
78
|
+
let size = CGSize(width: CGFloat(Self.imageWidth), height: CGFloat(Self.imageHeight))
|
|
79
|
+
guard let imageScale = Tools.scaleImage(image: image, toSize: size),
|
|
80
|
+
let image_data = Tools.convertUIImageToBitmapGray(image: imageScale) else {
|
|
81
|
+
return 0
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let laplace: [[Int]] = [[0, 1, 0], [1, -4, 1], [0, 1, 0]]
|
|
85
|
+
let laplaceSize = 3
|
|
86
|
+
|
|
87
|
+
var score = 0
|
|
88
|
+
for x in 0..<(Self.imageHeight - laplaceSize + 1) {
|
|
89
|
+
for y in 0..<(Self.imageWidth - laplaceSize + 1) {
|
|
90
|
+
var result = 0
|
|
91
|
+
for i in 0..<laplaceSize {
|
|
92
|
+
for j in 0..<laplaceSize {
|
|
93
|
+
result += (Int(image_data[(x + i) * Self.imageWidth + y + j]) & 0xFF) * laplace[i][j]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if result > Self.laplaceThreshold {
|
|
97
|
+
score += 1
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
image_data.deallocate()
|
|
102
|
+
|
|
103
|
+
return score
|
|
104
|
+
}
|
|
105
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Face Anti-Spoofing Plugin for Capacitor
|
|
7
|
+
* Provides passive liveness detection using TensorFlow Lite
|
|
8
|
+
*/
|
|
9
|
+
@objc(FaceAntiSpoofingPlugin)
|
|
10
|
+
public class FaceAntiSpoofingPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
11
|
+
public let identifier = "FaceAntiSpoofingPlugin"
|
|
12
|
+
public let jsName = "FaceAntiSpoofing"
|
|
13
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
14
|
+
CAPPluginMethod(name: "detect", returnType: CAPPluginReturnPromise)
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
private var faceAntiSpoofing: FaceAntiSpoofing?
|
|
18
|
+
private var mtcnn: MTCNN?
|
|
19
|
+
|
|
20
|
+
override public func load() {
|
|
21
|
+
do {
|
|
22
|
+
faceAntiSpoofing = try FaceAntiSpoofing()
|
|
23
|
+
mtcnn = try MTCNN()
|
|
24
|
+
print("[FaceAntiSpoofing] Models loaded successfully")
|
|
25
|
+
} catch {
|
|
26
|
+
print("[FaceAntiSpoofing] Failed to load models: \(error)")
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@objc func detect(_ call: CAPPluginCall) {
|
|
31
|
+
guard let image = call.getString("image"), !image.isEmpty else {
|
|
32
|
+
call.reject("Image data is required")
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
guard let fas = faceAntiSpoofing, let mtcnn = mtcnn else {
|
|
37
|
+
call.reject("Models not loaded. Please ensure the plugin is properly initialized.")
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
do {
|
|
42
|
+
guard let uiImage = decodeImage(from: image) else {
|
|
43
|
+
call.reject("Failed to decode image")
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let result = performAntiSpoofing(on: uiImage, fas: fas, mtcnn: mtcnn)
|
|
48
|
+
call.resolve(resultToJSObject(result))
|
|
49
|
+
} catch {
|
|
50
|
+
call.reject("Error during detection: \(error.localizedDescription)")
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private func decodeImage(from imageData: String) -> UIImage? {
|
|
55
|
+
// Check if it's a base64 string with data URI prefix
|
|
56
|
+
if imageData.hasPrefix("data:image/") {
|
|
57
|
+
if let commaIndex = imageData.range(of: ",")?.upperBound {
|
|
58
|
+
let base64String = String(imageData[commaIndex...])
|
|
59
|
+
if let imageData = Data(base64Encoded: base64String) {
|
|
60
|
+
return UIImage(data: imageData)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} else if imageData.hasPrefix("file://") {
|
|
64
|
+
let filePath = String(imageData.dropFirst(7))
|
|
65
|
+
let url = URL(fileURLWithPath: filePath)
|
|
66
|
+
if let imageData = try? Data(contentsOf: url) {
|
|
67
|
+
return UIImage(data: imageData)
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
// Try as raw base64
|
|
71
|
+
if let imageData = Data(base64Encoded: imageData) {
|
|
72
|
+
return UIImage(data: imageData)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return nil
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private func performAntiSpoofing(on image: UIImage, fas: FaceAntiSpoofing, mtcnn: MTCNN) -> DetectionResult {
|
|
79
|
+
var result = DetectionResult()
|
|
80
|
+
|
|
81
|
+
let boxes = mtcnn.detectFaces(image: image, minFaceSize: image.size.width / 8)
|
|
82
|
+
|
|
83
|
+
if boxes.isEmpty {
|
|
84
|
+
result.error = true
|
|
85
|
+
result.message = "No faces detected"
|
|
86
|
+
return result
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
guard let box1 = boxes.first else {
|
|
90
|
+
result.error = true
|
|
91
|
+
result.message = "No faces detected"
|
|
92
|
+
return result
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let alignedImage = Align.faceAlign(image: image, landmark: box1.landmark)
|
|
96
|
+
let boxes2 = mtcnn.detectFaces(image: alignedImage, minFaceSize: alignedImage.size.width / 8)
|
|
97
|
+
|
|
98
|
+
if boxes2.isEmpty {
|
|
99
|
+
result.error = true
|
|
100
|
+
result.message = "No faces detected"
|
|
101
|
+
return result
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
guard let box2 = boxes2.first else {
|
|
105
|
+
result.error = true
|
|
106
|
+
result.message = "No faces detected"
|
|
107
|
+
return result
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
var box = box2
|
|
111
|
+
box.toSquareShape()
|
|
112
|
+
box.limitSquare(w: Int(alignedImage.size.width), h: Int(alignedImage.size.height))
|
|
113
|
+
|
|
114
|
+
let rect = box.transform2Rect()
|
|
115
|
+
guard let croppedImage = Tools.cropImage(image: alignedImage, toRect: rect) else {
|
|
116
|
+
result.error = true
|
|
117
|
+
result.message = "Failed to crop image"
|
|
118
|
+
return result
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let laplace = fas.laplacian(image: croppedImage)
|
|
122
|
+
|
|
123
|
+
if laplace < FaceAntiSpoofing.laplacianThreshold {
|
|
124
|
+
result.error = false
|
|
125
|
+
result.score = "\(laplace)"
|
|
126
|
+
result.threshold = "\(FaceAntiSpoofing.laplacianThreshold)"
|
|
127
|
+
result.liveness = false
|
|
128
|
+
result.message = ""
|
|
129
|
+
} else {
|
|
130
|
+
let score = fas.antiSpoofing(image: croppedImage)
|
|
131
|
+
if score < FaceAntiSpoofing.threshold {
|
|
132
|
+
result.error = false
|
|
133
|
+
result.score = "\(score)"
|
|
134
|
+
result.threshold = "\(FaceAntiSpoofing.threshold)"
|
|
135
|
+
result.liveness = true
|
|
136
|
+
result.message = ""
|
|
137
|
+
} else {
|
|
138
|
+
result.error = false
|
|
139
|
+
result.score = "\(score)"
|
|
140
|
+
result.threshold = "\(FaceAntiSpoofing.threshold)"
|
|
141
|
+
result.liveness = false
|
|
142
|
+
result.message = "Sorry, we can't find a face or indicate that the photo is real."
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return result
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private func resultToJSObject(_ result: DetectionResult) -> [String: Any] {
|
|
150
|
+
return [
|
|
151
|
+
"error": result.error,
|
|
152
|
+
"liveness": result.liveness,
|
|
153
|
+
"score": result.score,
|
|
154
|
+
"threshold": result.threshold,
|
|
155
|
+
"message": result.message
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private struct DetectionResult {
|
|
160
|
+
var error = false
|
|
161
|
+
var liveness = false
|
|
162
|
+
var score = "0"
|
|
163
|
+
var threshold = "0"
|
|
164
|
+
var message = ""
|
|
165
|
+
}
|
|
166
|
+
}
|