上个月帮朋友公司做员工门禁考勤App,技术栈定的uni-app。需求清单里躺着一条“人脸识别打卡”。我当时心里清楚,uni-app自带的那套API里,根本没有给人脸识别留位置;插件市场里搜了一圈,要么是商业SDK套壳按终端数收费,要么只有Android没有iOS。问了两家服务商,授权费直接把项目预算打穿。最后只能走自己写插件的路:用uni-app官方的UTS能力,把原生人脸识别引擎封装成一个API插件,业务层继续写Vue,原生层去调系统框架和离线SDK。
这条路走下来,最大的感受是:UTS插件机制比很多人想象中能打,但坑也比想象中多。这篇就把整个制作过程、接口设计思路、双端实现要点和上线前躲不开的那些坑拆开写清楚,适合正在做uni-app人脸识别需求、又不想被插件市场绑架的团队参考。
1. 人脸识别为什么必须走原生插件:UTS补的是能力缺口
1.1 人脸识别的完整链路,哪一环是前端做不了的
人脸识别看起来是一个功能,背后其实是一条完整链路:图像采集、人脸检测、关键点定位、特征提取、特征比对,有的场景还要加上活体检测。翻遍前端的能力边界,能做的只有“把相机打开”和“把图片传到服务器”,后面那一串计算,无论是在本机做还是在云端做,基本都够不到原生层。
之前有人提过用纯前端方案,比如face-api.js配合WebGL跑模型。在iPhone和旗舰安卓机上,实时检测大约能跑到每秒几帧,看起来能用,但放到门禁打卡场景就不太现实了:低端机掉帧会掉到没法看,而且前端模型包动辄几MB,加载时间很难看。最麻烦的是精度和活体能力,一旦遇到照片、视频翻拍这类攻击,纯前端方案基本挡不住。
所以结论很直接:要做可靠的人脸识别,必须落到原生能力上。而uni-app项目要调用原生能力,除了写原生插件,就是走UTS。
1.2 UTS的运行机制:写TypeScript,编译成Kotlin和Swift
UTS全称是uni-app Universal TypeScript,语法接近TypeScript,但它不是跑在JS引擎里,而是在编译阶段被转换成对应的原生代码:Android侧编译成Kotlin,iOS侧编译成Swift。这就意味着你可以在UTS代码里直接import原生类,直接调用Android的CameraX、ML Kit,或者iOS的Vision框架,不需要像传统插件那样写一套Java、一套Objective-C,再搞一套JSBridge。
这里面有个关键点需要理解:UTS不是一门新语言,而是一层“语法糖编译桥”。你写的UTS代码最终会变成原生代码参与编译打包,所以它能访问的系统API和原生第三方SDK几乎没有上限。这也是UTS插件能做的人脸识别插件、而普通JS插件做不了的根本原因——普通JS插件只能通过renderjs或者内置API间接调用能力,能力边界被框得很死。
需要补充的是,UTS插件不解决算法问题。人脸检测、特征比对这些算法,还是要靠系统框架或第三方SDK来提供。UTS解决的是“把这些原生库接入uni-app工程”的工程化问题。想清楚这一层,后面设计接口时就不会跑偏。
1.3 先想清楚:用系统/离线SDK还是在线API
动手写插件前,先要确定算法来源。目前主流是三条路:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 系统框架(Android ML Kit / iOS Vision) | 免费、包体小、接入快 | 只能做人脸检测和关键点,不做特征比对;部分能力依赖Google服务 | 检测、区域遮挡判断、活体辅助 |
| 第三方离线SDK(虹软/百度离线/商汤离线等) | 精度高,自带特征提取和1:1比对 | 收费,SDK包体大,厂商申请流程麻烦 | 门禁打卡、考勤、实名认证 |
| 在线API(云厂商人脸识别接口) | 精度最高,活体方案成熟 | 依赖网络,有按次收费,隐私合规要求高 | 需要后台统一管理的场景 |
我做门禁打卡时的取舍是:人脸检测用Android的ML Kit和iOS的Vision,先把“画面里有没有人脸、人脸在哪个位置”搞定;特征提取和1:1比对用离线SDK,因为考勤场景经常在闸机、门禁这种网络不稳定的地方,不能把每一次打卡都压在云上。UTS插件本身对这三条路都是开放的,但接口层必须预留好扩展位,不然换算法SDK时插件要大改。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 搭一个UTS插件工程:目录、配置与最小可运行示例
2.1 用HBuilderX创建UTS插件,目录结构长什么样
创建UTS插件有两种方式:一种是在uni-app项目里手工创建uni_modules插件目录,另一种是通过HBuilderX的“新建UTS插件”向导。推荐用向导,它会自动生成规范的目录骨架。
一个最小可用的UTS插件目录大概是这样的:
code复制uni_modules/
└── uts-face-detect/
├── package.json
├── index.uts
├── android/
│ ├── src/
│ └── build.gradle
└── ios/
└── FaceDetectHelper.swift
这里每个目录的作用:
package.json:插件声明文件,定义插件id、版本号,比如"id": "uts-face-detect"。这个id就是后面业务层import时用的模块名。index.uts:插件门的入口文件,对外暴露方法、类、常量。业务层只能import到index.uts里export出去的东西。android/:放Kotlin代码和Android依赖配置,这里面的代码最终会参与Android打包。ios/:放Swift代码,参与iOS打包。
创建完之后,要在manifest.json的App模块配置里确认勾选了需要的原生模块,比如Camera相关能力。UTS插件本身不用在manifest里显式注册,但插件里依赖的原生模块必须勾选,否则真机调试时会报找不到类。
2.2 插件如何被业务层引用
把插件目录放进uni_modules后,业务层直接通过相对路径或别名import:
ts复制import { startDetect, stopDetect } from '@/uni_modules/uts-face-detect';
这里有个很容易踩的坑:UTS插件的文件路径如果带特殊字符或者层级太深,Android编译时可能报资源路径错误。我习惯把所有UTS插件放在uni_modules根目录下,不要在插件目录里再套多层无关目录。
2.3 第一个能跑的接口:从业务页面调用到原生
从工程模板到能跑通,其实就三步。第一步,在index.uts里定义一个导出函数:
uts复制export function getSystemName(): string {
// #ifdef APP-ANDROID
return getAndroidSystemName()
// #endif
// #ifdef APP-IOS
return getIOSSystemName()
// #endif
}
第二步,在Android的Kotlin文件里写真正的实现,并通过index.uts里的条件编译分平台调用。第三步,在业务页面里引入并调用:
ts复制import { getSystemName } from '@/uni_modules/uts-face-detect';
const name = getSystemName();
console.log(name);
跑通这个最小示例后,再做人脸识别才有底。因为一旦UTS插件在真机上能打日志、能返回值,说明插件链路是通的,后面所有问题都集中在具体实现和算法接入上。
3. 接口设计比写实现更重要:把检测流程抽象成API
3.1 面向业务层暴露哪些方法
很多人写UTS插件,习惯把第三方SDK的方法直接透传出来:SDK叫initFaceSDK,插件就叫initFaceSDK;SDK返回一个FaceInfo对象,插件也原样返回。这样做调试起来很痛苦,因为业务层根本不需要知道底层SDK长什么样。
我建议把接口收敛成业务真正关心的五个动作:
| 方法 | 用途 | 关键参数 |
|---|---|---|
| init | 初始化底层SDK,加载模型 | appId、licenseKey、识别阈值 |
| startDetect | 启动相机预览并开始实时检测 | 回调函数 |
| stopDetect | 停止预览和检测 | 无 |
| captureAndCompare | 抓取当前帧并与人脸底库/指定照片比对 | 底图路径或字节、超时时间 |
| release | 释放资源,退出页面时必调 | 无 |
这五个方法基本覆盖了门禁打卡、考勤签到、App内实名认证的典型场景。业务层只需要知道“我调startDetect开始识别,识别结果通过回调回来”,不关心底层用的是ML Kit还是虹软SDK。
3.2 参数和返回类型:别让业务层去解析原生对象
在UTS里定义返回类型时,不要直接返回原生对象,因为原生对象的字段命名、嵌套结构和业务侧的TypeScript类型不一定对得上。要定义一套独立的领域结构,在原生层做一次转换。
比如检测结果,我定义为这样一个TS/UTS类:
uts复制export class FaceDetectResult {
success: boolean = false
message: string = ''
hasFace: boolean = false
faceCount: number = 0
bounds: FaceBounds = new FaceBounds()
landmarks: FaceLandmark[] = []
score: number = 0
// 补充字段
timestamp: number = 0
}
export class FaceBounds {
left: number = 0
top: number = 0
width: number = 0
height: number = 0
}
export class FaceLandmark {
type: string = ''
x: number = 0
y: number = 0
}
这样设计的好处是,原生层无论返回的是ML Kit的Face对象,还是虹软的ASF_FaceInfo,都能统一映射到这套结构里。以后换算法SDK,只动插件内部,不动业务层代码。
3.3 回调与错误码:定位问题靠的是这套约定
异步检测的接口设计,关键是回调的规范和错误码。我在插件里定义了一套回调协议:
uts复制export type OnDetectCallback = (result: FaceDetectResult) => void;
export function startDetect(callback: OnDetectCallback): void {
// 原生实现里,每检测到一帧就回调一次
}
回调会非常频繁,业务层必须在回调里做节流或自行判断“当前是否在识别过程中”,不然会连续弹窗打卡成功。
错误码这部分很容易被忽略,但真正上线后,线上用户反馈“刷脸没反应”时,错误码就是唯一的线索。我在插件里约定了一套错误码,业务层可以直接展示给用户:
| 错误码 | 含义 | 业务建议 |
|---|---|---|
| 0 | 成功 | 正常处理 |
| 1001 | 相机权限被拒绝 | 引导去设置页开启权限 |
| 1002 | 相机初始化失败 | 提示用户重启App或更换机型 |
| 1003 | 未检测到人脸 | 提示用户靠近/摘口罩/调整光线 |
| 1004 | 人脸比对失败 | 提示用户重试 |
| 1005 | SDK未初始化 | 检查许可证或初始化流程 |
| 1006 | 系统资源不足 | 提示用户关闭后台App |
错误码要稳定,不能每次升级SDK就改。我见过有的插件把错误码跟第三方SDK的错误码直接绑定,SDK一升级,业务层到处报错。正确的做法是插件内部做一层错误码转换,把第三方错误归类成自己的业务错误码。
4. Android端实现:CameraX + ML Kit的组合与图像方向处理
4.1 为什么选CameraX而不是传统Camera2
Android端的人脸识别,相机采集是第一步。传统上有三条路:Camera旧API、Camera2、CameraX。我的选择是CameraX。
CameraX是Android官方基于Camera2封装的更上层API,它的优势非常实际:内部处理了生命周期绑定,页面销毁时自动释放相机;支持ImageAnalysis用例,可以直接拿到每一帧的图像数据;还内置了旋转和缩放的处理,省去大量样板代码。对于一个UTS插件来说,减少原生代码量就是减少出Bug的概率。
CameraX接入的典型流程:
kotlin复制val provider = ProcessCameraProvider.getInstance(context)
provider.addListener({
val cameraProvider = provider.get()
val preview = Preview.Builder().build()
val analysis = ImageAnalysis.Builder()
.setTargetResolution(Size(480, 640))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
analysis.setAnalyzer(executor) { imageProxy ->
val mediaImage = imageProxy.image
if (mediaImage != null) {
// 把这一帧交给检测器
detectFrame(mediaImage, imageProxy.imageInfo.rotationDegrees)
}
imageProxy.close() // 这句话必须执行,不然会卡住后续帧
}
cameraProvider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_FRONT_CAMERA,
preview,
analysis
)
}, ContextCompat.getMainExecutor(context))
这里有三个细节务必注意:
第一,imageProxy.close()不能漏。漏掉以后,ImageAnalysis会以为处理还没结束,不再下发新的帧,现象就是“相机画面卡在某一帧”。这个坑在集成到UTS插件里时更难发现,因为崩溃不会立刻出现,而是表现为识别越来越慢。
第二,ImageAnalysis的STRATEGY_KEEP_ONLY_LATEST策略,会在处理不过来时自动丢弃中间帧,保证始终分析最新帧。对实时人脸识别来说,这个策略是对的,但要注意设置setTargetResolution,不要用默认的全分辨率,否则每帧数据都很大,分析线程会过载。
第三,前置摄像头默认是镜像的,如果业务层要把检测画面渲染成“照镜子”的效果,需要在预览的PreviewView上做水平镜像。但注意,检测结果的坐标是基于原始图像的,如果画面镜像了,坐标映射也要跟着镜像处理,不然框的位置会左右颠倒。
4.2 把ML Kit的Face对象转成UTS能识别的结果
ML Kit的人脸检测接口简洁,官方已经封装好了,直接用就行:
kotlin复制val detector = FaceDetection.getClient(
FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
.setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
.setMinFaceSize(0.1f)
.build()
)
val image = InputImage.fromMediaImage(mediaImage, rotationDegrees)
detector.process(image)
.addOnSuccessListener { faces ->
val result = convertFaces(faces)
callback(result)
}
.addOnFailureListener { e ->
val errorResult = FaceDetectResult().apply {
success = false
message = e.message ?: "detect error"
}
callback(errorResult)
}
特别注意InputImage.fromMediaImage的旋转参数。它接收的是imageInfo.rotationDegrees,这个值表示摄像头传感器方向与屏幕方向之间的旋转角。如果不传或者传错,检测出来的脸框位置会偏移,甚至完全检测不到。
convertFaces要做的事是把ML Kit的Face对象转成我们在第3节定义的FaceDetectResult结构:
kotlin复制fun convertFaces(faces: List<Face>): FaceDetectResult {
val result = FaceDetectResult()
result.success = true
result.hasFace = faces.isNotEmpty()
result.faceCount = faces.size
faces.firstOrNull()?.let { face ->
val bounds = face.boundingBox
result.bounds.left = bounds.left.toFloat()
result.bounds.top = bounds.top.toFloat()
result.bounds.width = bounds.width().toFloat()
result.bounds.height = bounds.height().toFloat()
// 关键点:左眼、右眼、鼻尖、嘴角、轮廓点等
}
return result
}
这里的boundingBox单位是像素,对应的是原始图像坐标。业务层如果想要在页面上画框,需要知道当前预览画面的实际宽高和显示宽高的比例,再做一次缩放。
4.3 前置镜像/旋转角/低光:真机上最容易翻车的三件事
这三件事几乎每个项目都会遇到。
前置镜像问题:上面已经提过,镜像只影响预览显示,不影响检测算法的输入图像。但如果你要把检测结果叠加到预览画面上,就必须处理镜像映射。
旋转角问题:不要假设所有手机都是竖屏或横屏。折叠屏、平板、pad模式下,rotationDegrees可能是0、90、180、270中的任意值。建议在UTS插件里把这个角度透出给业务层,方便业务层根据角度做UI适配。
低光问题:ML Kit的人脸检测在弱光环境下的召回率会明显下降。我实测门禁机那种半昏暗环境下,检测率大概会掉10%到15%。缓解办法是让相机支持闪光灯,或者在检测前对图像做亮度增强。如果用的是第三方SDK,很多SDK自带低光增强,但会增加耗时,需要权衡。
5. iOS端实现:Vision框架接入与双端结果对齐
5.1 Vision框架做检测的代码路径
iOS端我直接用系统自带的Vision框架,人脸检测基础能力是免费且稳定的。对UTS插件来说,好处是不用额外引入第三方SDK,避免与隐私合规冲突。
Vision检测人脸的核心代码:
swift复制let request = VNDetectFaceRectanglesRequest()
let handler = VNImageRequestHandler(cgImage: cgImage, orientation: .up, options: [:])
try handler.perform([request])
if let results = request.results {
for observation in results {
let boundingBox = observation.boundingBox
// boundingBox 的坐标系是归一化的,原点在左下角
}
}
如果要更细的关键点信息,可以用VNDetectFaceLandmarksRequest,它能返回眼、鼻、嘴等部位的轮廓点。这些点同样是归一化坐标。
在UTS插件中写Swift时,要注意从UTS传递过来的参数类型转换。比如从业务层传来的是一个图片路径字符串,那在Swift侧要自己把它变成UIImage再转成CGImage:
swift复制let image = UIImage(contentsOfFile: path)
guard let cgImage = image?.cgImage else {
return
}
5.2 坐标系差异:iOS的boundingBox原点在左下角
这是iOS端最容易出问题的地方。iOS的Vision框架返回的boundingBox是归一化坐标,且原点在左下角;而Android侧通常默认原点在左上角。如果业务层同一套UI逻辑处理双端结果,就会遇到“iOS上人脸的框显示在屏幕下方”这种诡异问题。
解决办法是在UTS插件iOS实现里做一次坐标转换,把它统一成Android风格的原点左上:
swift复制let x = boundingBox.origin.x
let y = 1.0 - boundingBox.origin.y - boundingBox.size.height
let width = boundingBox.size.width
let height = boundingBox.size.height
这样业务层拿到的FaceBounds在双端语义一致,不需要在Vue页面里再写条件编译区分平台。做UTS插件的一个重要原则就是:平台的差异尽量在插件内部消化,不要让业务层感知。
5.3 双端结果怎么对齐到同一个领域模型
除了坐标系,还要统一几个字段的语义:
score:Android的ML Kit不直接提供置信度分数,只有headEulerAngleX/Y/Z这些姿态角;而部分第三方SDK会返回置信度。所以在UTS插件的Android侧,score可以用检测到人脸时的质量/角度来近似;iOS Vision的VNFaceObservation同样没有直接的置信度分数。如果业务层强依赖分数,建议在比对环节由比对SDK返回,因为比对SDK基本都有分数。landmarks的类型名:ML Kit的landmark类型是LEFT_EYE、RIGHT_EYE、NOSE_BASE等,Vision回归到VNFaceLandmarkRegion2D,命名不同。在UTS的FaceLandmark里,我用type字符串统一,比如leftEye、rightEye、noseBase,在双端转换时做好映射。
从性能上看,Vision的检测速度在A12芯片以上的iPhone上非常流畅,基本可以做到实时。但要特别注意:不要把Vision的检测放到主线程,否则相机预览会卡顿。UTS插件的Swift侧应该用DispatchQueue开异步处理。
6. 权限、隐私声明与崩溃排查:上线前的拦路虎
6.1 双端权限配置与运行时申请
人脸识别插件必然要申请相机权限,这看起来简单,但实际操作里有几个坑。
Android端,需要在AndroidManifest里声明相机权限,同时在运行时申请:
xml复制<uses-permission android:name="android.permission.CAMERA" />
在UTS插件里,运行时权限申请不能直接拿Activity去申请,因为页面上下文要正确传递。一般做法是在插件入口方法里接收一个上下文参数,或者在UTS层调用uni-app提供的权限API。我踩过的一个坑是:在插件里用的Activity已经被销毁,导致权限弹窗一闪而过。后来统一改成“由业务页面在进入人脸识别页面前先申请权限,插件启动时只检查权限”。
iOS端,只需在Info.plist里加用途描述:
xml复制<key>NSCameraUsageDescription</key>
<string>需要访问您的相机以进行人脸识别验证</string>
iOS如果不写这条描述,直接调用相机API会闪退。这个崩溃在UTS插件调试时尤其隐蔽,因为Xcode控制台会直接打印中文崩溃原因,但在HBuilderX的日志里不一定能第一时间看到。
6.2 “api scope未声明”这类隐私问题的处理
很多uni-app开发者都遇到过类似chooseimage:fail api scope is not declared in the privacy agreement的报错。这虽然不是UTS插件独有的问题,但在自研人脸识别插件时同样会出现。
原因很简单:应用市场(特别是国内安卓市场)在审核时会读取App的隐私政策,检查App实际调用的权限和API是否都在隐私政策里声明。如果你在插件里调用了相机、相册或者存储权限,但隐私政策文本里没有对应说明,市场审核就会以“隐私协议不一致”拒审。
我整理了一个自查清单,每次上架前都会过一遍:
- 隐私政策里是否明确写了“使用相机进行人脸识别/活体检测”;
- 是否写明人脸识别数据的存储位置和存储期限;
- 是否写明用户如何删除人脸特征数据;
- 插件申请的所有权限是否都在权限声明列表里;
- 最小化权限:人脸识别场景不要申请短信、通讯录等无关权限。
这个环节的价值经常被低估。很多UTS插件功能开发完了,卡在应用市场审核上,来回折腾一个月,最后发现是隐私声明没写好。
6.3 闪退/黑屏的定位思路
UTS插件里闪退,和原生App闪退的排查思路完全不同。因为错误栈可能出现在原生层,但运行环境是uni-app。
我的经验是分层排查:
首先看业务层日志。UTS插件里的console.log输出会出现在HBuilderX控制台,先看有没有报错。如果业务层日志正常,再怀疑原生层。
然后看原生日志。Android真机可以通过adb logcat看系统级崩溃信息,iOS可以连Xcode看Console。UTS插件里如果调用了不存在的原生方法,或者空指针,崩溃信息会指向对应的Kotlin/Swift类。
最后是“黑屏”问题。人脸识别页面黑屏,多数不是权限问题,而是相机预览Surface没有正确绑定到页面容器。在UTS插件开发中,相机预览通常需要原生View。如果你在UTS插件里返回了一个原生View,并让业务层挂载,要确认这个View的生命周期。很多黑屏是因为页面在onLoad时就启动相机,但预览View还没布局完成,导致SurfaceHolder是null。
一种稳妥的做法是:业务层在页面onReady之后再去调startDetect,并设置一个短暂的延迟(比如100ms),等View完全挂载。
7. 调试、打包与性能优化:从能跑到好用
7.1 自定义基座、真机调试与离线打包
UTS插件不能直接在HBuilderX标准基座上跑。标准基座只包含官方内置模块,不带你的自定义原生代码。所以每次改动UTS插件,都必须“制作自定义基座”,然后运行到真机上调试。
自定义基座的操作流程是:HBuilderX菜单 → 运行 → 运行到手机或模拟器 → 制作自定义调试基座,然后在运行选项里勾选“使用自定义基座”。自定义基座会把你当前项目的所有UTS插件和原生配置打包进去,这个过程会拉取依赖,联网状态下通常几分钟完成。
上线时的打包有两种方式:
- 云打包:在HBuilderX里直接“发行 → 原生App-云打包”。云端有对应版本的原生环境,你只需要在manifest里填写包名、证书、图标等信息。
- 离线打包:下载对应HBuilderX版本的Android/iOS离线SDK,用Android Studio或Xcode手动集成插件。这种方式适合需要深度定制原生工程、或者有自动化打包体系的团队。
7.2 HBuilderX版本与本地SDK版本不同步的问题
“uniapp本地打包sdk版本与hbuilderx版本”这个问题在热门搜索里很常见,也是UTS插件项目里非常现实的坑。
HBuilderX每次升级,UTS编译器和原生SDK都会同步更新。如果你用HBuilderX 4.x创建的自定义基座,开发的UTS插件上传到某个环境,但离线打包时用了旧版本的离线SDK,很可能出现UTS插件运行时报“方法找不到”或直接编译失败。
我吃过一次亏:项目里的UTS插件用新版本编译器生成,内部引用了原生SDK里新增的一个API函数,但离线打包那边还是旧版SDK,结果一启动就崩溃。排查了半天,最后发现是版本不一致。
解决思路是:在项目里固定HBuilderX版本,团队内部所有成员统一用同一版本;离线打包时,下载离线SDK的版本号必须和HBuilderX的版本号一致。不要轻易升级HBuilderX,升级前先看发布说明里UTS相关的变化。
7.3 拉帧率、压缩图像与活体检测的现实选择
最后说一下性能优化。人脸识别在门禁/考勤场景下的核心体验指标是“能不能刷得上、刷得快不快”。
帧率控制是第一个优化点。不要对CameraX的每一帧都跑检测,那会让手机发烫,而且检测结果其实不需要每帧都处理。我通常在插件里控制检测频率:每秒最多处理8到10帧。实现方式是在Analyzer里根据时间戳过滤,或者用HandlerThread加上延迟。
kotlin复制private var lastDetectTime = 0L
private val detectInterval = 100L // 100ms一帧
override fun analyze(imageProxy: ImageProxy) {
val now = System.currentTimeMillis()
if (now - lastDetectTime < detectInterval) {
imageProxy.close()
return
}
lastDetectTime = now
// 继续检测流程
}
图像压缩是第二个优化点。人脸检测不需要原始分辨率,把ImageAnalysis的目标分辨率设置到640x480甚至320x240,检测时间可以大幅下降。但如果用第三方SDK做特征提取,要注意特征提取对图像质量有最低要求,不能压得太狠。我建议检测和特征提取用两个不同分辨率的流程:检测用低分辨率,特征提取用较高分辨率的照片帧。
活体检测是第三个点,也是成本上最容易失算的点。ML Kit和Vision本身不提供活体判定,只能返回眼睛是否睁开、头部角度等信息。要防照片攻击,通常需要业务层配合做“眨眼检测”或“左右转头”动作引导,然后根据关键点变化判断是不是真人。如果客户要求高安全级别的活体,那还是得采购商业SDK。UTS插件里可以把基础动作活体做成一个独立方法,比如startLivenessDetect(callback),在里面跑一套规则引擎。
以我个人的经验,做这类插件最忌讳“一上来就写代码”。先把接口边界、错误码、跨平台差异这三件事想清楚,原生实现只是时间问题。UTS插件的维护成本不在第一次写通,而在后续换底层SDK、适配新系统版本时,如果接口层做得够干净,这些变化都能被吸收在插件内部,业务层的Vue代码一行都不用动。这大概就是UTS插件这门手艺最值钱的地方。
