@sssxyd/face-liveness-detector 0.4.0-alpha.2 → 0.4.0-alpha.4
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/dist/index.esm.js +176 -63
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +176 -63
- package/dist/index.js.map +1 -1
- package/dist/types/face-detection-engine.d.ts +10 -0
- package/dist/types/face-detection-engine.d.ts.map +1 -1
- package/dist/types/face-detection-state.d.ts +1 -0
- package/dist/types/face-detection-state.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -173,9 +173,7 @@ function mergeOptions(userConfig) {
|
|
|
173
173
|
* Provides on, off, once, and emit methods for event-driven architecture
|
|
174
174
|
*/
|
|
175
175
|
class SimpleEventEmitter {
|
|
176
|
-
|
|
177
|
-
this.listeners = new Map();
|
|
178
|
-
}
|
|
176
|
+
listeners = new Map();
|
|
179
177
|
/**
|
|
180
178
|
* Register an event listener
|
|
181
179
|
* @param event - Event name
|
|
@@ -1680,6 +1678,22 @@ function matToGray(cv, mat) {
|
|
|
1680
1678
|
* 运动检测结果
|
|
1681
1679
|
*/
|
|
1682
1680
|
class MotionDetectionResult {
|
|
1681
|
+
// 总体运动评分 (0-1)
|
|
1682
|
+
motionScore;
|
|
1683
|
+
// 人脸区域的光流幅度
|
|
1684
|
+
opticalFlowMagnitude;
|
|
1685
|
+
// 关键点稳定性评分 (0 = 像照片一样稳定, 1 = 自然运动)
|
|
1686
|
+
keypointVariance;
|
|
1687
|
+
// 眼睛区域运动强度
|
|
1688
|
+
eyeMotionScore;
|
|
1689
|
+
// 嘴巴区域运动强度
|
|
1690
|
+
mouthMotionScore;
|
|
1691
|
+
// 检测到的运动类型 ('none' | 'rotation' | 'translation' | 'breathing' | 'micro_expression')
|
|
1692
|
+
motionType;
|
|
1693
|
+
// 基于运动的总体活体性判断
|
|
1694
|
+
isLively;
|
|
1695
|
+
// 详细调试信息
|
|
1696
|
+
details;
|
|
1683
1697
|
constructor(motionScore, opticalFlowMagnitude, keypointVariance, eyeMotionScore, mouthMotionScore, motionType, isLively, details) {
|
|
1684
1698
|
this.motionScore = motionScore;
|
|
1685
1699
|
this.opticalFlowMagnitude = opticalFlowMagnitude;
|
|
@@ -1723,17 +1737,25 @@ class MotionDetectionResult {
|
|
|
1723
1737
|
* 使用光流、关键点跟踪和面部特征分析
|
|
1724
1738
|
*/
|
|
1725
1739
|
class MotionLivenessDetector {
|
|
1740
|
+
// 配置及默认值
|
|
1741
|
+
minMotionThreshold;
|
|
1742
|
+
minKeypointVariance;
|
|
1743
|
+
frameBufferSize;
|
|
1744
|
+
eyeAspectRatioThreshold;
|
|
1745
|
+
motionConsistencyThreshold;
|
|
1746
|
+
minOpticalFlowThreshold;
|
|
1747
|
+
strictPhotoDetection;
|
|
1748
|
+
// 状态
|
|
1749
|
+
frameBuffer = []; // 存储 cv.Mat (gray)
|
|
1750
|
+
keypointHistory = [];
|
|
1751
|
+
faceAreaHistory = [];
|
|
1752
|
+
eyeAspectRatioHistory = [];
|
|
1753
|
+
mouthAspectRatioHistory = [];
|
|
1754
|
+
opticalFlowHistory = [];
|
|
1755
|
+
pupilSizeHistory = [];
|
|
1756
|
+
// OpenCV 实例
|
|
1757
|
+
cv = null;
|
|
1726
1758
|
constructor(options = {}) {
|
|
1727
|
-
// 状态
|
|
1728
|
-
this.frameBuffer = []; // 存储 cv.Mat (gray)
|
|
1729
|
-
this.keypointHistory = [];
|
|
1730
|
-
this.faceAreaHistory = [];
|
|
1731
|
-
this.eyeAspectRatioHistory = [];
|
|
1732
|
-
this.mouthAspectRatioHistory = [];
|
|
1733
|
-
this.opticalFlowHistory = [];
|
|
1734
|
-
this.pupilSizeHistory = [];
|
|
1735
|
-
// OpenCV 实例
|
|
1736
|
-
this.cv = null;
|
|
1737
1759
|
// 用提供的选项或默认值设置配置
|
|
1738
1760
|
this.minMotionThreshold = options.minMotionThreshold ?? 0.15;
|
|
1739
1761
|
this.minKeypointVariance = options.minKeypointVariance ?? 0.02;
|
|
@@ -3668,6 +3690,16 @@ function stringToDetectionStrategy(value, defaultValue) {
|
|
|
3668
3690
|
* 优化版屏幕采集检测结果
|
|
3669
3691
|
*/
|
|
3670
3692
|
class ScreenCaptureDetectionResult {
|
|
3693
|
+
isScreenCapture;
|
|
3694
|
+
confidenceScore;
|
|
3695
|
+
// 实际执行的检测方法结果
|
|
3696
|
+
executedMethods;
|
|
3697
|
+
// 未执行的方法(因为已经有结论)
|
|
3698
|
+
skippedMethods;
|
|
3699
|
+
riskLevel;
|
|
3700
|
+
processingTimeMs;
|
|
3701
|
+
strategy;
|
|
3702
|
+
debug;
|
|
3671
3703
|
constructor(isScreenCapture, confidenceScore, executedMethods, riskLevel, processingTimeMs, strategy, skippedMethods, debug) {
|
|
3672
3704
|
this.isScreenCapture = isScreenCapture;
|
|
3673
3705
|
this.confidenceScore = confidenceScore;
|
|
@@ -3696,10 +3728,13 @@ class ScreenCaptureDetectionResult {
|
|
|
3696
3728
|
* 使用级联检测策略,支持多种模式以平衡速度和精准度
|
|
3697
3729
|
*/
|
|
3698
3730
|
class ScreenCaptureDetector {
|
|
3731
|
+
cv = null;
|
|
3732
|
+
confidenceThreshold = 0.6;
|
|
3733
|
+
detectionStrategy = DetectionStrategy.ADAPTIVE;
|
|
3734
|
+
moirePatternConfig;
|
|
3735
|
+
screenColorConfig;
|
|
3736
|
+
rgbEmissionConfig;
|
|
3699
3737
|
constructor(options = {}) {
|
|
3700
|
-
this.cv = null;
|
|
3701
|
-
this.confidenceThreshold = 0.6;
|
|
3702
|
-
this.detectionStrategy = DetectionStrategy.ADAPTIVE;
|
|
3703
3738
|
this.confidenceThreshold = options.confidenceThreshold ?? 0.6;
|
|
3704
3739
|
this.detectionStrategy = options.detectionStrategy
|
|
3705
3740
|
? stringToDetectionStrategy(options.detectionStrategy, DetectionStrategy.ADAPTIVE)
|
|
@@ -3995,21 +4030,21 @@ class ScreenCaptureDetector {
|
|
|
3995
4030
|
* Internal detection state interface
|
|
3996
4031
|
*/
|
|
3997
4032
|
class DetectionState {
|
|
4033
|
+
period = DetectionPeriod.DETECT;
|
|
4034
|
+
startTime = performance.now();
|
|
4035
|
+
collectCount = 0;
|
|
4036
|
+
suspectedFraudsCount = 0;
|
|
4037
|
+
bestQualityScore = 0;
|
|
4038
|
+
bestFrameImage = null;
|
|
4039
|
+
bestFaceImage = null;
|
|
4040
|
+
completedActions = new Set();
|
|
4041
|
+
currentAction = null;
|
|
4042
|
+
actionVerifyTimeout = null;
|
|
4043
|
+
lastFrontalScore = 1;
|
|
4044
|
+
motionDetector = null;
|
|
4045
|
+
liveness = false;
|
|
4046
|
+
screenDetector = null;
|
|
3998
4047
|
constructor(options) {
|
|
3999
|
-
this.period = DetectionPeriod.DETECT;
|
|
4000
|
-
this.startTime = performance.now();
|
|
4001
|
-
this.collectCount = 0;
|
|
4002
|
-
this.suspectedFraudsCount = 0;
|
|
4003
|
-
this.bestQualityScore = 0;
|
|
4004
|
-
this.bestFrameImage = null;
|
|
4005
|
-
this.bestFaceImage = null;
|
|
4006
|
-
this.completedActions = new Set();
|
|
4007
|
-
this.currentAction = null;
|
|
4008
|
-
this.actionVerifyTimeout = null;
|
|
4009
|
-
this.lastFrontalScore = 1;
|
|
4010
|
-
this.motionDetector = null;
|
|
4011
|
-
this.liveness = false;
|
|
4012
|
-
this.screenDetector = null;
|
|
4013
4048
|
Object.assign(this, options);
|
|
4014
4049
|
}
|
|
4015
4050
|
reset() {
|
|
@@ -4050,6 +4085,10 @@ class DetectionState {
|
|
|
4050
4085
|
this.completedActions.add(this.currentAction);
|
|
4051
4086
|
this.currentAction = null;
|
|
4052
4087
|
}
|
|
4088
|
+
setCVInstance(cvInstance) {
|
|
4089
|
+
this.motionDetector?.setCVInstance(cvInstance);
|
|
4090
|
+
this.screenDetector?.setCVInstance(cvInstance);
|
|
4091
|
+
}
|
|
4053
4092
|
/**
|
|
4054
4093
|
* Clear action verify timeout
|
|
4055
4094
|
*/
|
|
@@ -4100,35 +4139,95 @@ function createDetectionState(options) {
|
|
|
4100
4139
|
* Provides core detection logic without UI dependencies
|
|
4101
4140
|
*/
|
|
4102
4141
|
class FaceDetectionEngine extends SimpleEventEmitter {
|
|
4142
|
+
options;
|
|
4143
|
+
// OpenCV instance
|
|
4144
|
+
cv = null;
|
|
4145
|
+
human = null;
|
|
4146
|
+
engineState = EngineState.IDLE;
|
|
4147
|
+
// 视频及保存当前帧图片的Canvas元素
|
|
4148
|
+
videoElement = null;
|
|
4149
|
+
stream = null;
|
|
4150
|
+
frameCanvasElement = null;
|
|
4151
|
+
frameCanvasContext = null;
|
|
4152
|
+
faceCanvasElement = null;
|
|
4153
|
+
faceCanvasContext = null;
|
|
4154
|
+
detectionFrameId = null;
|
|
4155
|
+
actualVideoWidth = 0;
|
|
4156
|
+
actualVideoHeight = 0;
|
|
4157
|
+
detectionState;
|
|
4103
4158
|
/**
|
|
4104
4159
|
* Constructor
|
|
4105
4160
|
* @param config - Configuration object
|
|
4106
4161
|
*/
|
|
4107
4162
|
constructor(options) {
|
|
4108
4163
|
super();
|
|
4109
|
-
// OpenCV instance
|
|
4110
|
-
this.cv = null;
|
|
4111
|
-
this.human = null;
|
|
4112
|
-
this.engineState = EngineState.IDLE;
|
|
4113
|
-
// 视频及保存当前帧图片的Canvas元素
|
|
4114
|
-
this.videoElement = null;
|
|
4115
|
-
this.stream = null;
|
|
4116
|
-
this.frameCanvasElement = null;
|
|
4117
|
-
this.frameCanvasContext = null;
|
|
4118
|
-
this.faceCanvasElement = null;
|
|
4119
|
-
this.faceCanvasContext = null;
|
|
4120
|
-
this.detectionFrameId = null;
|
|
4121
|
-
this.actualVideoWidth = 0;
|
|
4122
|
-
this.actualVideoHeight = 0;
|
|
4123
4164
|
this.options = mergeOptions(options);
|
|
4124
4165
|
this.detectionState = createDetectionState(this.options);
|
|
4125
4166
|
}
|
|
4167
|
+
/**
|
|
4168
|
+
* 提取错误信息的辅助方法 - 处理各种错误类型
|
|
4169
|
+
* @param error - 任意类型的错误对象
|
|
4170
|
+
* @returns 包含错误消息和堆栈的对象
|
|
4171
|
+
*/
|
|
4172
|
+
extractErrorInfo(error) {
|
|
4173
|
+
// 处理 Error 实例
|
|
4174
|
+
if (error instanceof Error) {
|
|
4175
|
+
let causeStr;
|
|
4176
|
+
if (error.cause) {
|
|
4177
|
+
causeStr = error.cause instanceof Error ? error.cause.message : String(error.cause);
|
|
4178
|
+
}
|
|
4179
|
+
return {
|
|
4180
|
+
message: error.message || 'Unknown error',
|
|
4181
|
+
stack: error.stack || this.getStackTrace(),
|
|
4182
|
+
name: error.name,
|
|
4183
|
+
cause: causeStr
|
|
4184
|
+
};
|
|
4185
|
+
}
|
|
4186
|
+
// 处理其他对象类型
|
|
4187
|
+
if (typeof error === 'object' && error !== null) {
|
|
4188
|
+
let causeStr;
|
|
4189
|
+
if ('cause' in error) {
|
|
4190
|
+
const cause = error.cause;
|
|
4191
|
+
causeStr = cause instanceof Error ? cause.message : String(cause);
|
|
4192
|
+
}
|
|
4193
|
+
return {
|
|
4194
|
+
message: error.message || JSON.stringify(error),
|
|
4195
|
+
stack: error.stack || this.getStackTrace(),
|
|
4196
|
+
name: error.name,
|
|
4197
|
+
cause: causeStr
|
|
4198
|
+
};
|
|
4199
|
+
}
|
|
4200
|
+
// 处理基本类型(string, number 等)
|
|
4201
|
+
return {
|
|
4202
|
+
message: String(error),
|
|
4203
|
+
stack: this.getStackTrace()
|
|
4204
|
+
};
|
|
4205
|
+
}
|
|
4206
|
+
/**
|
|
4207
|
+
* 获取当前调用栈信息
|
|
4208
|
+
*/
|
|
4209
|
+
getStackTrace() {
|
|
4210
|
+
try {
|
|
4211
|
+
// 创建一个Error对象来获取堆栈
|
|
4212
|
+
const err = new Error();
|
|
4213
|
+
if (err.stack) {
|
|
4214
|
+
// 移除前两行(Error 和 getStackTrace 本身)
|
|
4215
|
+
const lines = err.stack.split('\n');
|
|
4216
|
+
return lines.slice(2).join('\n') || 'Stack trace unavailable';
|
|
4217
|
+
}
|
|
4218
|
+
return 'Stack trace unavailable';
|
|
4219
|
+
}
|
|
4220
|
+
catch {
|
|
4221
|
+
return 'Stack trace unavailable';
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4126
4224
|
updateOptions(options) {
|
|
4127
4225
|
if (this.engineState == EngineState.DETECTING) {
|
|
4128
4226
|
this.stopDetection(false);
|
|
4129
4227
|
}
|
|
4130
4228
|
this.options = mergeOptions(options);
|
|
4131
4229
|
this.detectionState = createDetectionState(this.options);
|
|
4230
|
+
this.detectionState.setCVInstance(this.cv);
|
|
4132
4231
|
}
|
|
4133
4232
|
getEngineState() {
|
|
4134
4233
|
return this.engineState;
|
|
@@ -4167,10 +4266,8 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4167
4266
|
console.log('[FaceDetectionEngine] OpenCV loaded successfully', {
|
|
4168
4267
|
version: cv_version
|
|
4169
4268
|
});
|
|
4170
|
-
// Inject OpenCV instance into motion detector
|
|
4171
|
-
|
|
4172
|
-
this.detectionState.motionDetector.cv = cv;
|
|
4173
|
-
}
|
|
4269
|
+
// Inject OpenCV instance into motion detector and screen detector
|
|
4270
|
+
this.detectionState.setCVInstance(this.cv);
|
|
4174
4271
|
// Load Human.js
|
|
4175
4272
|
console.log('[FaceDetectionEngine] Loading Human.js models...');
|
|
4176
4273
|
this.emitDebug('initialization', 'Loading Human.js...');
|
|
@@ -4179,12 +4276,14 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4179
4276
|
this.human = await loadHuman(this.options.human_model_path, this.options.tensorflow_wasm_path, this.options.tensorflow_backend);
|
|
4180
4277
|
}
|
|
4181
4278
|
catch (humanError) {
|
|
4182
|
-
const
|
|
4183
|
-
const
|
|
4279
|
+
const errorInfo = this.extractErrorInfo(humanError);
|
|
4280
|
+
const errorMsg = errorInfo.message;
|
|
4184
4281
|
// 分析错误类型,提供针对性的建议
|
|
4185
4282
|
let errorContext = {
|
|
4186
4283
|
error: errorMsg,
|
|
4187
|
-
stack,
|
|
4284
|
+
stack: errorInfo.stack,
|
|
4285
|
+
name: errorInfo.name,
|
|
4286
|
+
cause: errorInfo.cause,
|
|
4188
4287
|
userAgent: navigator.userAgent,
|
|
4189
4288
|
platform: navigator.userAgentData?.platform || 'unknown',
|
|
4190
4289
|
browser: detectBrowserEngine(navigator.userAgent),
|
|
@@ -4288,7 +4387,8 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4288
4387
|
this.emitDebug('initialization', 'Engine initialized and ready', loadedData);
|
|
4289
4388
|
}
|
|
4290
4389
|
catch (error) {
|
|
4291
|
-
const
|
|
4390
|
+
const errorInfo = this.extractErrorInfo(error);
|
|
4391
|
+
const errorMsg = errorInfo.message;
|
|
4292
4392
|
this.emit('detector-loaded', {
|
|
4293
4393
|
success: false,
|
|
4294
4394
|
error: errorMsg
|
|
@@ -4299,7 +4399,9 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4299
4399
|
});
|
|
4300
4400
|
this.emitDebug('initialization', 'Failed to load libraries', {
|
|
4301
4401
|
error: errorMsg,
|
|
4302
|
-
stack:
|
|
4402
|
+
stack: errorInfo.stack,
|
|
4403
|
+
name: errorInfo.name,
|
|
4404
|
+
cause: errorInfo.cause
|
|
4303
4405
|
}, 'error');
|
|
4304
4406
|
}
|
|
4305
4407
|
finally {
|
|
@@ -4426,10 +4528,13 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4426
4528
|
this.emitDebug('video-setup', 'Detection started');
|
|
4427
4529
|
}
|
|
4428
4530
|
catch (error) {
|
|
4429
|
-
const
|
|
4531
|
+
const errorInfo = this.extractErrorInfo(error);
|
|
4532
|
+
const errorMsg = errorInfo.message;
|
|
4430
4533
|
this.emitDebug('video-setup', 'Failed to start detection', {
|
|
4431
4534
|
error: errorMsg,
|
|
4432
|
-
stack:
|
|
4535
|
+
stack: errorInfo.stack,
|
|
4536
|
+
name: errorInfo.name,
|
|
4537
|
+
cause: errorInfo.cause
|
|
4433
4538
|
}, 'error');
|
|
4434
4539
|
this.emit('detector-error', {
|
|
4435
4540
|
code: ErrorCode.STREAM_ACQUISITION_FAILED,
|
|
@@ -4527,10 +4632,12 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4527
4632
|
result = await this.human.detect(this.videoElement);
|
|
4528
4633
|
}
|
|
4529
4634
|
catch (detectError) {
|
|
4530
|
-
const
|
|
4635
|
+
const errorInfo = this.extractErrorInfo(detectError);
|
|
4636
|
+
const errorMsg = errorInfo.message;
|
|
4531
4637
|
this.emitDebug('detection', 'Human.detect() call failed', {
|
|
4532
4638
|
error: errorMsg,
|
|
4533
|
-
stack:
|
|
4639
|
+
stack: errorInfo.stack,
|
|
4640
|
+
name: errorInfo.name,
|
|
4534
4641
|
hasHuman: !!this.human,
|
|
4535
4642
|
humanVersion: this.human?.version,
|
|
4536
4643
|
videoReadyState: this.videoElement?.readyState,
|
|
@@ -4555,10 +4662,13 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4555
4662
|
}
|
|
4556
4663
|
}
|
|
4557
4664
|
catch (error) {
|
|
4558
|
-
const
|
|
4665
|
+
const errorInfo = this.extractErrorInfo(error);
|
|
4666
|
+
const errorMsg = errorInfo.message;
|
|
4559
4667
|
this.emitDebug('detection', 'Unexpected error in detection loop', {
|
|
4560
4668
|
error: errorMsg,
|
|
4561
|
-
stack:
|
|
4669
|
+
stack: errorInfo.stack,
|
|
4670
|
+
name: errorInfo.name,
|
|
4671
|
+
cause: errorInfo.cause
|
|
4562
4672
|
}, 'error');
|
|
4563
4673
|
this.scheduleNextDetection(this.options.detect_error_retry_delay);
|
|
4564
4674
|
}
|
|
@@ -4747,10 +4857,13 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4747
4857
|
}
|
|
4748
4858
|
}
|
|
4749
4859
|
catch (error) {
|
|
4750
|
-
const
|
|
4860
|
+
const errorInfo = this.extractErrorInfo(error);
|
|
4861
|
+
const errorMsg = errorInfo.message;
|
|
4751
4862
|
this.emitDebug('detection', 'Unexpected error in single face handling', {
|
|
4752
4863
|
error: errorMsg,
|
|
4753
|
-
stack:
|
|
4864
|
+
stack: errorInfo.stack,
|
|
4865
|
+
name: errorInfo.name,
|
|
4866
|
+
cause: errorInfo.cause
|
|
4754
4867
|
}, 'error');
|
|
4755
4868
|
this.scheduleNextDetection(this.options.detect_error_retry_delay);
|
|
4756
4869
|
}
|
|
@@ -5380,6 +5493,8 @@ async function preloadResources() {
|
|
|
5380
5493
|
* Wrapper around FaceDetectionEngine optimized for UniApp
|
|
5381
5494
|
*/
|
|
5382
5495
|
class UniAppFaceDetectionEngine extends FaceDetectionEngine {
|
|
5496
|
+
resourcesInitialized = false;
|
|
5497
|
+
resourcesPreloaded = false;
|
|
5383
5498
|
/**
|
|
5384
5499
|
* Constructor
|
|
5385
5500
|
* @param config - Configuration object
|
|
@@ -5393,8 +5508,6 @@ class UniAppFaceDetectionEngine extends FaceDetectionEngine {
|
|
|
5393
5508
|
tensorflow_wasm_path: config?.tensorflow_wasm_path || getWasmPath()
|
|
5394
5509
|
};
|
|
5395
5510
|
super(finalConfig);
|
|
5396
|
-
this.resourcesInitialized = false;
|
|
5397
|
-
this.resourcesPreloaded = false;
|
|
5398
5511
|
// Initialize UniApp resources
|
|
5399
5512
|
if (uniAppConfig.isUniApp) {
|
|
5400
5513
|
initializeUniAppResources();
|