@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.js
CHANGED
|
@@ -195,9 +195,7 @@
|
|
|
195
195
|
* Provides on, off, once, and emit methods for event-driven architecture
|
|
196
196
|
*/
|
|
197
197
|
class SimpleEventEmitter {
|
|
198
|
-
|
|
199
|
-
this.listeners = new Map();
|
|
200
|
-
}
|
|
198
|
+
listeners = new Map();
|
|
201
199
|
/**
|
|
202
200
|
* Register an event listener
|
|
203
201
|
* @param event - Event name
|
|
@@ -1702,6 +1700,22 @@
|
|
|
1702
1700
|
* 运动检测结果
|
|
1703
1701
|
*/
|
|
1704
1702
|
class MotionDetectionResult {
|
|
1703
|
+
// 总体运动评分 (0-1)
|
|
1704
|
+
motionScore;
|
|
1705
|
+
// 人脸区域的光流幅度
|
|
1706
|
+
opticalFlowMagnitude;
|
|
1707
|
+
// 关键点稳定性评分 (0 = 像照片一样稳定, 1 = 自然运动)
|
|
1708
|
+
keypointVariance;
|
|
1709
|
+
// 眼睛区域运动强度
|
|
1710
|
+
eyeMotionScore;
|
|
1711
|
+
// 嘴巴区域运动强度
|
|
1712
|
+
mouthMotionScore;
|
|
1713
|
+
// 检测到的运动类型 ('none' | 'rotation' | 'translation' | 'breathing' | 'micro_expression')
|
|
1714
|
+
motionType;
|
|
1715
|
+
// 基于运动的总体活体性判断
|
|
1716
|
+
isLively;
|
|
1717
|
+
// 详细调试信息
|
|
1718
|
+
details;
|
|
1705
1719
|
constructor(motionScore, opticalFlowMagnitude, keypointVariance, eyeMotionScore, mouthMotionScore, motionType, isLively, details) {
|
|
1706
1720
|
this.motionScore = motionScore;
|
|
1707
1721
|
this.opticalFlowMagnitude = opticalFlowMagnitude;
|
|
@@ -1745,17 +1759,25 @@
|
|
|
1745
1759
|
* 使用光流、关键点跟踪和面部特征分析
|
|
1746
1760
|
*/
|
|
1747
1761
|
class MotionLivenessDetector {
|
|
1762
|
+
// 配置及默认值
|
|
1763
|
+
minMotionThreshold;
|
|
1764
|
+
minKeypointVariance;
|
|
1765
|
+
frameBufferSize;
|
|
1766
|
+
eyeAspectRatioThreshold;
|
|
1767
|
+
motionConsistencyThreshold;
|
|
1768
|
+
minOpticalFlowThreshold;
|
|
1769
|
+
strictPhotoDetection;
|
|
1770
|
+
// 状态
|
|
1771
|
+
frameBuffer = []; // 存储 cv.Mat (gray)
|
|
1772
|
+
keypointHistory = [];
|
|
1773
|
+
faceAreaHistory = [];
|
|
1774
|
+
eyeAspectRatioHistory = [];
|
|
1775
|
+
mouthAspectRatioHistory = [];
|
|
1776
|
+
opticalFlowHistory = [];
|
|
1777
|
+
pupilSizeHistory = [];
|
|
1778
|
+
// OpenCV 实例
|
|
1779
|
+
cv = null;
|
|
1748
1780
|
constructor(options = {}) {
|
|
1749
|
-
// 状态
|
|
1750
|
-
this.frameBuffer = []; // 存储 cv.Mat (gray)
|
|
1751
|
-
this.keypointHistory = [];
|
|
1752
|
-
this.faceAreaHistory = [];
|
|
1753
|
-
this.eyeAspectRatioHistory = [];
|
|
1754
|
-
this.mouthAspectRatioHistory = [];
|
|
1755
|
-
this.opticalFlowHistory = [];
|
|
1756
|
-
this.pupilSizeHistory = [];
|
|
1757
|
-
// OpenCV 实例
|
|
1758
|
-
this.cv = null;
|
|
1759
1781
|
// 用提供的选项或默认值设置配置
|
|
1760
1782
|
this.minMotionThreshold = options.minMotionThreshold ?? 0.15;
|
|
1761
1783
|
this.minKeypointVariance = options.minKeypointVariance ?? 0.02;
|
|
@@ -3690,6 +3712,16 @@
|
|
|
3690
3712
|
* 优化版屏幕采集检测结果
|
|
3691
3713
|
*/
|
|
3692
3714
|
class ScreenCaptureDetectionResult {
|
|
3715
|
+
isScreenCapture;
|
|
3716
|
+
confidenceScore;
|
|
3717
|
+
// 实际执行的检测方法结果
|
|
3718
|
+
executedMethods;
|
|
3719
|
+
// 未执行的方法(因为已经有结论)
|
|
3720
|
+
skippedMethods;
|
|
3721
|
+
riskLevel;
|
|
3722
|
+
processingTimeMs;
|
|
3723
|
+
strategy;
|
|
3724
|
+
debug;
|
|
3693
3725
|
constructor(isScreenCapture, confidenceScore, executedMethods, riskLevel, processingTimeMs, strategy, skippedMethods, debug) {
|
|
3694
3726
|
this.isScreenCapture = isScreenCapture;
|
|
3695
3727
|
this.confidenceScore = confidenceScore;
|
|
@@ -3718,10 +3750,13 @@
|
|
|
3718
3750
|
* 使用级联检测策略,支持多种模式以平衡速度和精准度
|
|
3719
3751
|
*/
|
|
3720
3752
|
class ScreenCaptureDetector {
|
|
3753
|
+
cv = null;
|
|
3754
|
+
confidenceThreshold = 0.6;
|
|
3755
|
+
detectionStrategy = DetectionStrategy.ADAPTIVE;
|
|
3756
|
+
moirePatternConfig;
|
|
3757
|
+
screenColorConfig;
|
|
3758
|
+
rgbEmissionConfig;
|
|
3721
3759
|
constructor(options = {}) {
|
|
3722
|
-
this.cv = null;
|
|
3723
|
-
this.confidenceThreshold = 0.6;
|
|
3724
|
-
this.detectionStrategy = DetectionStrategy.ADAPTIVE;
|
|
3725
3760
|
this.confidenceThreshold = options.confidenceThreshold ?? 0.6;
|
|
3726
3761
|
this.detectionStrategy = options.detectionStrategy
|
|
3727
3762
|
? stringToDetectionStrategy(options.detectionStrategy, DetectionStrategy.ADAPTIVE)
|
|
@@ -4017,21 +4052,21 @@
|
|
|
4017
4052
|
* Internal detection state interface
|
|
4018
4053
|
*/
|
|
4019
4054
|
class DetectionState {
|
|
4055
|
+
period = exports.DetectionPeriod.DETECT;
|
|
4056
|
+
startTime = performance.now();
|
|
4057
|
+
collectCount = 0;
|
|
4058
|
+
suspectedFraudsCount = 0;
|
|
4059
|
+
bestQualityScore = 0;
|
|
4060
|
+
bestFrameImage = null;
|
|
4061
|
+
bestFaceImage = null;
|
|
4062
|
+
completedActions = new Set();
|
|
4063
|
+
currentAction = null;
|
|
4064
|
+
actionVerifyTimeout = null;
|
|
4065
|
+
lastFrontalScore = 1;
|
|
4066
|
+
motionDetector = null;
|
|
4067
|
+
liveness = false;
|
|
4068
|
+
screenDetector = null;
|
|
4020
4069
|
constructor(options) {
|
|
4021
|
-
this.period = exports.DetectionPeriod.DETECT;
|
|
4022
|
-
this.startTime = performance.now();
|
|
4023
|
-
this.collectCount = 0;
|
|
4024
|
-
this.suspectedFraudsCount = 0;
|
|
4025
|
-
this.bestQualityScore = 0;
|
|
4026
|
-
this.bestFrameImage = null;
|
|
4027
|
-
this.bestFaceImage = null;
|
|
4028
|
-
this.completedActions = new Set();
|
|
4029
|
-
this.currentAction = null;
|
|
4030
|
-
this.actionVerifyTimeout = null;
|
|
4031
|
-
this.lastFrontalScore = 1;
|
|
4032
|
-
this.motionDetector = null;
|
|
4033
|
-
this.liveness = false;
|
|
4034
|
-
this.screenDetector = null;
|
|
4035
4070
|
Object.assign(this, options);
|
|
4036
4071
|
}
|
|
4037
4072
|
reset() {
|
|
@@ -4072,6 +4107,10 @@
|
|
|
4072
4107
|
this.completedActions.add(this.currentAction);
|
|
4073
4108
|
this.currentAction = null;
|
|
4074
4109
|
}
|
|
4110
|
+
setCVInstance(cvInstance) {
|
|
4111
|
+
this.motionDetector?.setCVInstance(cvInstance);
|
|
4112
|
+
this.screenDetector?.setCVInstance(cvInstance);
|
|
4113
|
+
}
|
|
4075
4114
|
/**
|
|
4076
4115
|
* Clear action verify timeout
|
|
4077
4116
|
*/
|
|
@@ -4122,35 +4161,95 @@
|
|
|
4122
4161
|
* Provides core detection logic without UI dependencies
|
|
4123
4162
|
*/
|
|
4124
4163
|
class FaceDetectionEngine extends SimpleEventEmitter {
|
|
4164
|
+
options;
|
|
4165
|
+
// OpenCV instance
|
|
4166
|
+
cv = null;
|
|
4167
|
+
human = null;
|
|
4168
|
+
engineState = exports.EngineState.IDLE;
|
|
4169
|
+
// 视频及保存当前帧图片的Canvas元素
|
|
4170
|
+
videoElement = null;
|
|
4171
|
+
stream = null;
|
|
4172
|
+
frameCanvasElement = null;
|
|
4173
|
+
frameCanvasContext = null;
|
|
4174
|
+
faceCanvasElement = null;
|
|
4175
|
+
faceCanvasContext = null;
|
|
4176
|
+
detectionFrameId = null;
|
|
4177
|
+
actualVideoWidth = 0;
|
|
4178
|
+
actualVideoHeight = 0;
|
|
4179
|
+
detectionState;
|
|
4125
4180
|
/**
|
|
4126
4181
|
* Constructor
|
|
4127
4182
|
* @param config - Configuration object
|
|
4128
4183
|
*/
|
|
4129
4184
|
constructor(options) {
|
|
4130
4185
|
super();
|
|
4131
|
-
// OpenCV instance
|
|
4132
|
-
this.cv = null;
|
|
4133
|
-
this.human = null;
|
|
4134
|
-
this.engineState = exports.EngineState.IDLE;
|
|
4135
|
-
// 视频及保存当前帧图片的Canvas元素
|
|
4136
|
-
this.videoElement = null;
|
|
4137
|
-
this.stream = null;
|
|
4138
|
-
this.frameCanvasElement = null;
|
|
4139
|
-
this.frameCanvasContext = null;
|
|
4140
|
-
this.faceCanvasElement = null;
|
|
4141
|
-
this.faceCanvasContext = null;
|
|
4142
|
-
this.detectionFrameId = null;
|
|
4143
|
-
this.actualVideoWidth = 0;
|
|
4144
|
-
this.actualVideoHeight = 0;
|
|
4145
4186
|
this.options = mergeOptions(options);
|
|
4146
4187
|
this.detectionState = createDetectionState(this.options);
|
|
4147
4188
|
}
|
|
4189
|
+
/**
|
|
4190
|
+
* 提取错误信息的辅助方法 - 处理各种错误类型
|
|
4191
|
+
* @param error - 任意类型的错误对象
|
|
4192
|
+
* @returns 包含错误消息和堆栈的对象
|
|
4193
|
+
*/
|
|
4194
|
+
extractErrorInfo(error) {
|
|
4195
|
+
// 处理 Error 实例
|
|
4196
|
+
if (error instanceof Error) {
|
|
4197
|
+
let causeStr;
|
|
4198
|
+
if (error.cause) {
|
|
4199
|
+
causeStr = error.cause instanceof Error ? error.cause.message : String(error.cause);
|
|
4200
|
+
}
|
|
4201
|
+
return {
|
|
4202
|
+
message: error.message || 'Unknown error',
|
|
4203
|
+
stack: error.stack || this.getStackTrace(),
|
|
4204
|
+
name: error.name,
|
|
4205
|
+
cause: causeStr
|
|
4206
|
+
};
|
|
4207
|
+
}
|
|
4208
|
+
// 处理其他对象类型
|
|
4209
|
+
if (typeof error === 'object' && error !== null) {
|
|
4210
|
+
let causeStr;
|
|
4211
|
+
if ('cause' in error) {
|
|
4212
|
+
const cause = error.cause;
|
|
4213
|
+
causeStr = cause instanceof Error ? cause.message : String(cause);
|
|
4214
|
+
}
|
|
4215
|
+
return {
|
|
4216
|
+
message: error.message || JSON.stringify(error),
|
|
4217
|
+
stack: error.stack || this.getStackTrace(),
|
|
4218
|
+
name: error.name,
|
|
4219
|
+
cause: causeStr
|
|
4220
|
+
};
|
|
4221
|
+
}
|
|
4222
|
+
// 处理基本类型(string, number 等)
|
|
4223
|
+
return {
|
|
4224
|
+
message: String(error),
|
|
4225
|
+
stack: this.getStackTrace()
|
|
4226
|
+
};
|
|
4227
|
+
}
|
|
4228
|
+
/**
|
|
4229
|
+
* 获取当前调用栈信息
|
|
4230
|
+
*/
|
|
4231
|
+
getStackTrace() {
|
|
4232
|
+
try {
|
|
4233
|
+
// 创建一个Error对象来获取堆栈
|
|
4234
|
+
const err = new Error();
|
|
4235
|
+
if (err.stack) {
|
|
4236
|
+
// 移除前两行(Error 和 getStackTrace 本身)
|
|
4237
|
+
const lines = err.stack.split('\n');
|
|
4238
|
+
return lines.slice(2).join('\n') || 'Stack trace unavailable';
|
|
4239
|
+
}
|
|
4240
|
+
return 'Stack trace unavailable';
|
|
4241
|
+
}
|
|
4242
|
+
catch {
|
|
4243
|
+
return 'Stack trace unavailable';
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4148
4246
|
updateOptions(options) {
|
|
4149
4247
|
if (this.engineState == exports.EngineState.DETECTING) {
|
|
4150
4248
|
this.stopDetection(false);
|
|
4151
4249
|
}
|
|
4152
4250
|
this.options = mergeOptions(options);
|
|
4153
4251
|
this.detectionState = createDetectionState(this.options);
|
|
4252
|
+
this.detectionState.setCVInstance(this.cv);
|
|
4154
4253
|
}
|
|
4155
4254
|
getEngineState() {
|
|
4156
4255
|
return this.engineState;
|
|
@@ -4189,10 +4288,8 @@
|
|
|
4189
4288
|
console.log('[FaceDetectionEngine] OpenCV loaded successfully', {
|
|
4190
4289
|
version: cv_version
|
|
4191
4290
|
});
|
|
4192
|
-
// Inject OpenCV instance into motion detector
|
|
4193
|
-
|
|
4194
|
-
this.detectionState.motionDetector.cv = cv;
|
|
4195
|
-
}
|
|
4291
|
+
// Inject OpenCV instance into motion detector and screen detector
|
|
4292
|
+
this.detectionState.setCVInstance(this.cv);
|
|
4196
4293
|
// Load Human.js
|
|
4197
4294
|
console.log('[FaceDetectionEngine] Loading Human.js models...');
|
|
4198
4295
|
this.emitDebug('initialization', 'Loading Human.js...');
|
|
@@ -4201,12 +4298,14 @@
|
|
|
4201
4298
|
this.human = await loadHuman(this.options.human_model_path, this.options.tensorflow_wasm_path, this.options.tensorflow_backend);
|
|
4202
4299
|
}
|
|
4203
4300
|
catch (humanError) {
|
|
4204
|
-
const
|
|
4205
|
-
const
|
|
4301
|
+
const errorInfo = this.extractErrorInfo(humanError);
|
|
4302
|
+
const errorMsg = errorInfo.message;
|
|
4206
4303
|
// 分析错误类型,提供针对性的建议
|
|
4207
4304
|
let errorContext = {
|
|
4208
4305
|
error: errorMsg,
|
|
4209
|
-
stack,
|
|
4306
|
+
stack: errorInfo.stack,
|
|
4307
|
+
name: errorInfo.name,
|
|
4308
|
+
cause: errorInfo.cause,
|
|
4210
4309
|
userAgent: navigator.userAgent,
|
|
4211
4310
|
platform: navigator.userAgentData?.platform || 'unknown',
|
|
4212
4311
|
browser: detectBrowserEngine(navigator.userAgent),
|
|
@@ -4310,7 +4409,8 @@
|
|
|
4310
4409
|
this.emitDebug('initialization', 'Engine initialized and ready', loadedData);
|
|
4311
4410
|
}
|
|
4312
4411
|
catch (error) {
|
|
4313
|
-
const
|
|
4412
|
+
const errorInfo = this.extractErrorInfo(error);
|
|
4413
|
+
const errorMsg = errorInfo.message;
|
|
4314
4414
|
this.emit('detector-loaded', {
|
|
4315
4415
|
success: false,
|
|
4316
4416
|
error: errorMsg
|
|
@@ -4321,7 +4421,9 @@
|
|
|
4321
4421
|
});
|
|
4322
4422
|
this.emitDebug('initialization', 'Failed to load libraries', {
|
|
4323
4423
|
error: errorMsg,
|
|
4324
|
-
stack:
|
|
4424
|
+
stack: errorInfo.stack,
|
|
4425
|
+
name: errorInfo.name,
|
|
4426
|
+
cause: errorInfo.cause
|
|
4325
4427
|
}, 'error');
|
|
4326
4428
|
}
|
|
4327
4429
|
finally {
|
|
@@ -4448,10 +4550,13 @@
|
|
|
4448
4550
|
this.emitDebug('video-setup', 'Detection started');
|
|
4449
4551
|
}
|
|
4450
4552
|
catch (error) {
|
|
4451
|
-
const
|
|
4553
|
+
const errorInfo = this.extractErrorInfo(error);
|
|
4554
|
+
const errorMsg = errorInfo.message;
|
|
4452
4555
|
this.emitDebug('video-setup', 'Failed to start detection', {
|
|
4453
4556
|
error: errorMsg,
|
|
4454
|
-
stack:
|
|
4557
|
+
stack: errorInfo.stack,
|
|
4558
|
+
name: errorInfo.name,
|
|
4559
|
+
cause: errorInfo.cause
|
|
4455
4560
|
}, 'error');
|
|
4456
4561
|
this.emit('detector-error', {
|
|
4457
4562
|
code: exports.ErrorCode.STREAM_ACQUISITION_FAILED,
|
|
@@ -4549,10 +4654,12 @@
|
|
|
4549
4654
|
result = await this.human.detect(this.videoElement);
|
|
4550
4655
|
}
|
|
4551
4656
|
catch (detectError) {
|
|
4552
|
-
const
|
|
4657
|
+
const errorInfo = this.extractErrorInfo(detectError);
|
|
4658
|
+
const errorMsg = errorInfo.message;
|
|
4553
4659
|
this.emitDebug('detection', 'Human.detect() call failed', {
|
|
4554
4660
|
error: errorMsg,
|
|
4555
|
-
stack:
|
|
4661
|
+
stack: errorInfo.stack,
|
|
4662
|
+
name: errorInfo.name,
|
|
4556
4663
|
hasHuman: !!this.human,
|
|
4557
4664
|
humanVersion: this.human?.version,
|
|
4558
4665
|
videoReadyState: this.videoElement?.readyState,
|
|
@@ -4577,10 +4684,13 @@
|
|
|
4577
4684
|
}
|
|
4578
4685
|
}
|
|
4579
4686
|
catch (error) {
|
|
4580
|
-
const
|
|
4687
|
+
const errorInfo = this.extractErrorInfo(error);
|
|
4688
|
+
const errorMsg = errorInfo.message;
|
|
4581
4689
|
this.emitDebug('detection', 'Unexpected error in detection loop', {
|
|
4582
4690
|
error: errorMsg,
|
|
4583
|
-
stack:
|
|
4691
|
+
stack: errorInfo.stack,
|
|
4692
|
+
name: errorInfo.name,
|
|
4693
|
+
cause: errorInfo.cause
|
|
4584
4694
|
}, 'error');
|
|
4585
4695
|
this.scheduleNextDetection(this.options.detect_error_retry_delay);
|
|
4586
4696
|
}
|
|
@@ -4769,10 +4879,13 @@
|
|
|
4769
4879
|
}
|
|
4770
4880
|
}
|
|
4771
4881
|
catch (error) {
|
|
4772
|
-
const
|
|
4882
|
+
const errorInfo = this.extractErrorInfo(error);
|
|
4883
|
+
const errorMsg = errorInfo.message;
|
|
4773
4884
|
this.emitDebug('detection', 'Unexpected error in single face handling', {
|
|
4774
4885
|
error: errorMsg,
|
|
4775
|
-
stack:
|
|
4886
|
+
stack: errorInfo.stack,
|
|
4887
|
+
name: errorInfo.name,
|
|
4888
|
+
cause: errorInfo.cause
|
|
4776
4889
|
}, 'error');
|
|
4777
4890
|
this.scheduleNextDetection(this.options.detect_error_retry_delay);
|
|
4778
4891
|
}
|
|
@@ -5402,6 +5515,8 @@
|
|
|
5402
5515
|
* Wrapper around FaceDetectionEngine optimized for UniApp
|
|
5403
5516
|
*/
|
|
5404
5517
|
class UniAppFaceDetectionEngine extends FaceDetectionEngine {
|
|
5518
|
+
resourcesInitialized = false;
|
|
5519
|
+
resourcesPreloaded = false;
|
|
5405
5520
|
/**
|
|
5406
5521
|
* Constructor
|
|
5407
5522
|
* @param config - Configuration object
|
|
@@ -5415,8 +5530,6 @@
|
|
|
5415
5530
|
tensorflow_wasm_path: config?.tensorflow_wasm_path || getWasmPath()
|
|
5416
5531
|
};
|
|
5417
5532
|
super(finalConfig);
|
|
5418
|
-
this.resourcesInitialized = false;
|
|
5419
|
-
this.resourcesPreloaded = false;
|
|
5420
5533
|
// Initialize UniApp resources
|
|
5421
5534
|
if (uniAppConfig.isUniApp) {
|
|
5422
5535
|
initializeUniAppResources();
|