This commit is contained in:
2026-09-24 16:25:22 +08:00
commit 7428184f01
1198 changed files with 314515 additions and 0 deletions
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
package="io.dcloud.uni_modules.xFaceDetection">
<uses-permission android:name="android.permission.VIBRATE" />
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<application>
<meta-data android:name="UTS_MLKIT_xFaceDetection" android:value="xFaceDetection" />
<activity android:name="uts.sdk.modules.xFaceDetection.DemoActivity"></activity>
</application>
</manifest>
@@ -0,0 +1,51 @@
import MediaPlayer from "android.media.MediaPlayer"
import File from 'java.io.File';
import FileOutputStream from 'java.io.FileOutputStream';
export class xAudio {
audio:MediaPlayer;
constructor(){
this.audio = new MediaPlayer();
}
play(src:string){
this.audio.stop();
this.audio.release()
this.audio = new MediaPlayer();
let assetManager = UTSAndroid.getAppContext()!.getAssets();
// 加载free.mp3 资源
const listAssets = assetManager.list("");
let filename = src+".mp3";
let isExites = listAssets?.contains(filename)??false
if(!isExites) return;
let afd = assetManager.openFd(filename);
try {
// 使用android 自带的媒体组件进行播放
let mediaPlayer = this.audio;
mediaPlayer.setDataSource(afd.getFileDescriptor(),afd.getStartOffset(), afd.getLength());
mediaPlayer.prepare();
mediaPlayer.start();
class audioCompleted implements MediaPlayer.OnCompletionListener{
override onCompletion(m:MediaPlayer|null){
// if(m!=null){
// m.release()
// }
}
}
mediaPlayer.setOnCompletionListener(new audioCompleted())
} catch (e: Exception) {
console.log("AudioPlayer", "Error setting data source: ",e)
} finally {
// 确保关闭文件描述符
afd.close()
}
}
stop(){
// this.audio.stop();
}
destory(){
// this.audio.release()
}
}
@@ -0,0 +1,548 @@
import ListView from 'android.widget.ListView';
import FrameLayout from 'android.widget.FrameLayout';
import CameraManager from 'android.hardware.camera2.CameraManager'
import CameraDevice from 'android.hardware.camera2.CameraDevice'
import StateCallback from 'android.hardware.camera2.CameraDevice.StateCallback'
import CameraCaptureSession from 'android.hardware.camera2.CameraCaptureSession'
import Camera2CameraControl from 'androidx.camera.camera2.interop.Camera2CameraControl'
import Camera2CameraInfo from 'androidx.camera.camera2.interop.Camera2CameraInfo'
import CaptureRequest from 'android.hardware.camera2.CaptureRequest'
import TotalCaptureResult from 'android.hardware.camera2.TotalCaptureResult'
import StreamConfigurationMap from 'android.hardware.camera2.params.StreamConfigurationMap'
import MeteringRectangle from 'android.hardware.camera2.params.MeteringRectangle'
import OutputConfiguration from 'android.hardware.camera2.params.OutputConfiguration'
import SessionConfiguration from 'android.hardware.camera2.params.SessionConfiguration'
import CameraCharacteristics from 'android.hardware.camera2.CameraCharacteristics'
import CameraMetadata from 'android.hardware.camera2.CameraMetadata'
import Range from 'android.util.Range';
import InputImage from "com.google.mlkit.vision.common.InputImage"
import Executor from 'java.util.concurrent.Executor'
import Looper from 'android.os.Looper'
import ImageFormat from 'android.graphics.ImageFormat'
import Size from 'android.util.Size'
import SurfaceTexture from 'android.graphics.SurfaceTexture'
import Handler from 'android.os.Handler'
import Surface from 'android.view.Surface'
import Image from 'android.media.Image'
import LinearLayout from 'android.widget.LinearLayout';
import Intent from 'android.content.Intent';
import Integer from 'java.lang.Integer';
import ByteOrder from 'java.nio.ByteOrder';
import SurfaceHolder from 'android.view.SurfaceHolder'
import SurfaceView from 'android.view.SurfaceView'
import TextureView from 'android.view.TextureView'
import View from 'android.view.View'
import Context from 'android.content.Context'
import Manifest from "android.Manifest";
import Uri from 'android.net.Uri';
import File from 'java.io.File'
import System from 'java.lang.System'
import Activity from 'android.app.Activity';
import Bundle from 'android.os.Bundle';
import Intent from 'android.content.Intent';
import IntentFilter from 'android.content.IntentFilter';
import Rect from 'android.graphics.Rect';
import Matrix from 'android.graphics.Matrix'
import ImageReader from 'android.media.ImageReader'
import OnImageAvailableListener from 'android.media.ImageReader.OnImageAvailableListener'
import ByteBuffer from 'java.nio.ByteBuffer'
import MediaStore from 'android.provider.MediaStore'
import Plane from 'android.media.Image.Plane'
import Bitmap from "android.graphics.Bitmap"
import BitmapFactory from "android.graphics.BitmapFactory"
import Vibrator from "android.os.Vibrator"
import Animation from "android.view.animation.Animation"
import ScaleAnimation from "android.view.animation.ScaleAnimation"
import AnimationUtils from "android.view.animation.AnimationUtils"
import Color from "android.graphics.Color"
import Window from "android.view.Window"
import WindowManager from "android.view.WindowManager"
import TextView from "android.widget.TextView"
import RelativeLayout from "android.widget.RelativeLayout"
import ImageView from "android.widget.ImageView"
import HandlerThread from 'android.os.HandlerThread'
import YuvImage from 'android.graphics.YuvImage'
import ByteArrayOutputStream from 'java.io.ByteArrayOutputStream'
import GradientDrawable from 'android.graphics.drawable.GradientDrawable'
import MotionEvent from 'android.view.MotionEvent';
import ViewGroup from 'android.view.ViewGroup';
import IOException from 'java.io.IOException'
import InputStream from 'java.io.InputStream'
// import BarcodeScanner from "com.google.mlkit.vision.barcode.BarcodeScanner"
// import BarcodeScannerOptions from "com.google.mlkit.vision.barcode.BarcodeScannerOptions"
// import BarcodeScanning from "com.google.mlkit.vision.barcode.BarcodeScanning"
// import ZoomSuggestionOptions from "com.google.mlkit.vision.barcode.ZoomSuggestionOptions"
// import ZoomCallback from "com.google.mlkit.vision.barcode.ZoomSuggestionOptions.ZoomCallback"
// import Barcode from "com.google.mlkit.vision.barcode.common.Barcode"
// import InputImage from "com.google.mlkit.vision.common.InputImage"
import Task from "com.google.android.gms.tasks.Task"
import List from "java.util.List"
// x
import ProcessCameraProvider from "androidx.camera.lifecycle.ProcessCameraProvider"
import PreviewView from "androidx.camera.view.PreviewView"
import CameraController from "androidx.camera.view.CameraController"
import CameraSelector from "androidx.camera.core.CameraSelector"
import Preview from 'androidx.camera.core.Preview'
import Camera from 'androidx.camera.core.Camera'
import LifecycleObserver from "androidx.lifecycle.LifecycleObserver"
import LifecycleOwner from "androidx.lifecycle.LifecycleOwner"
import OnLifecycleEvent from "androidx.lifecycle.OnLifecycleEvent"
import ImageAnalysis from 'androidx.camera.core.ImageAnalysis'
import ImageProxy from 'androidx.camera.core.ImageProxy'
import ImageCapture from 'androidx.camera.core.ImageCapture';
import AspectRatio from 'androidx.camera.core.AspectRatio';
import CameraInfo from 'androidx.camera.core.CameraInfo';
import CameraControl from 'androidx.camera.core.CameraControl'
import NonNull from 'androidx.annotation.NonNull'
import ExecutorService from 'java.util.concurrent.ExecutorService'
import Executors from 'java.util.concurrent.Executors'
// import TargetResolution from 'androidx.camera.core.ImageAnalysis.TargetResolution'
import {decodeImage} from "./index.uts"
type buffoptsPlanesType = {
data : ByteBuffer,
getRowStride : number,
getPixelStride : number,
}
type buffoptsType = {
width : number,
height : number,
format : number,
rect : Rect,
planes : buffoptsPlanesType[],
}
import { AUTH_CALL_BACK_TYPE, CAMERA_PHOTO_SIZE,XCHECKFACE_ENTER_CALL,XFACE_EVENT_SUCCESS } from "../interface.uts"
import ContextCompat from 'androidx.core.content.ContextCompat';
import FileOutputStream from 'java.io.FileOutputStream';
import PreviewConfig from 'androidx.camera.core.impl.PreviewConfig';
import VideoSource from 'android.media.MediaRecorder.VideoSource';
import VideoCapture from 'androidx.camera.video.VideoCapture';
import OutputFileOptions from 'androidx.camera.core.ImageCapture.OutputFileOptions';
import Recorder from 'androidx.camera.video.Recorder';
import Quality from 'androidx.camera.video.Quality';
import QualitySelector from 'androidx.camera.video.QualitySelector';
import OutputFileResults from 'androidx.camera.core.ImageCapture.OutputFileResults';
import FileDescriptor from 'java.io.FileDescriptor';
import FileOutputOptions from 'androidx.camera.video.FileOutputOptions';
import VideoRecordEvent from 'androidx.camera.video.VideoRecordEvent';
import Consumer from 'androidx.core.util.Consumer';
import Recording from 'androidx.camera.video.Recording';
import ResolutionCorrector from 'androidx.camera.camera2.internal.compat.workaround.ResolutionCorrector';
import AspectRatioStrategy from 'androidx.camera.core.resolutionselector.AspectRatioStrategy';
import ResolutionSelector from 'androidx.camera.core.resolutionselector.ResolutionSelector';
type cameraListNameType = { id : string, device : CameraCharacteristics }
type TAKE_CALL_BACK = (path : string) => void;
let callFuntake = (path : string) => { }
let callVideoFun = (path : string) => { }
let callVideoFaceFun= (img : XFACE_EVENT_SUCCESS[],imgBit:Bitmap|null) => { }
let checking = true
function px2dp(n : number) : number {
const mets = UTSAndroid.getAppContext()!.resources!.getDisplayMetrics()
return mets.density * n
}
function getUid(rdix = 1, length = 12) : string {
let ix = "";
// #ifdef APP
ix = Math.floor(Math.random() * rdix * Math.floor(Math.random() * Date.now())).toString().substring(0, length as Int);
// #endif
return ix;
}
export class xCamera {
parentView : FrameLayout;
viewBox_width = 0
viewBox_height = 0
imgPointsBycanmarea = [] as RelativeLayout[]
/** 手动震动服务 */
vibrator = null as null | Vibrator
context : Context;
// 相机旋转的角度。
cameraRate = new Map<Number, Number>([
[Surface.ROTATION_0, 0],
[Surface.ROTATION_90, 0],
[Surface.ROTATION_180, 0],
[Surface.ROTATION_270, 0]
])
// 相机服务
cameraService : CameraManager
// 相机列表,name,id
cameraListName = new Map<string, cameraListNameType>();
//
cameraPreviewSize = { width: 800, height: 800 } as CAMERA_PHOTO_SIZE
// 当前预览的什么是设备
cameraDevice : CameraDevice | null = null
cameraDeviceCaptureSession : CameraCaptureSession | null = null
sufaceHolderView : Surface | null = null
// 相机区域视图
cameraView : PreviewView | null = null;
cameraProvider : ProcessCameraProvider | null = null
camera : Camera | null = null;
cameraPreview : Preview | null = null;
videoCapture : VideoCapture<Recorder> | null = null;
// Recording
recoderVideoObj:Recording|null = null;
// 是否拍照中
isDecoderQring = false
// 当前相机是否正在预览中。
isOpeningCameraing = false;
callFunEvent : TAKE_CALL_BACK = (str : string) => { }
//摄像头朝向,默认是向后置摄像头.
CamerDeviceDir = CameraSelector.LENS_FACING_FRONT
//摄像头的是否开
flashMode = false
// 拍照模式,还是录像模式.
takeModelType = 'video'; //photo,video
private PHOTO_TYPE = "image/jpeg"
private RATIO_4_3_VALUE = 4.0 / 3.0
private RATIO_16_9_VALUE = 16.0 / 9.0
constructor(GroupView : FrameLayout) {
this.context = UTSAndroid.getAppContext()! as Context;
this.parentView = GroupView;
this.cameraService = this.context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
this.vibrator = this.context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator;
this._createCameraView()
}
/** 创建相机预览区域 */
private _createCameraView() {
let sv = new PreviewView(UTSAndroid.getAppContext()!)
sv.scaleType = PreviewView.ScaleType.FILL_CENTER
// 让相机的高和宽与组件对齐。
let layaout = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
sv.setLayoutParams(layaout)
this.cameraView = sv;
this.parentView.addView(this.cameraView!)
}
openCamera() {
let _this = this;
if (this.isOpeningCameraing) {
this.closeCamera()
}
_this._openCamera();
}
startRecoderVideo(call : (path : string) => void) {
callVideoFun = call;
let _this = this;
if(_this.recoderVideoObj?.isClosed()==false){
_this.recoderVideoObj?.resume()
}else{
if (this.isOpeningCameraing||this.recoderVideoObj!=null) {
this.stopRecoderVideo()
this.closeCamera()
}
_this.takeModelType = 'video'
_this._openCamera();
}
}
pauseRecoderVideo(){
let _this = this;
_this.recoderVideoObj?.pause()
}
stopRecoderVideo() {
let _this = this;
_this.takeModelType = 'photo'
_this.recoderVideoObj?.stop()
_this.recoderVideoObj?.close()
_this.recoderVideoObj = null;
_this.videoCapture = null;
// closeCamera();
}
setCallEvent(call : TAKE_CALL_BACK) {
this.callFunEvent = call;
}
setCheckFaceEnter(call:XCHECKFACE_ENTER_CALL){
callVideoFaceFun = call
}
getCamraRotation() : Number {
return 0
}
closeCamera() {
let t = this;
class IntentRunable2 extends Runnable {
override run() {
t.cameraProvider?.unbindAll()
t.cameraPreview?.setSurfaceProvider(null)
t.isOpeningCameraing = false;
}
}
// 确保在主线程中关闭相机
new Handler(Looper.getMainLooper()).post(new IntentRunable2());
}
private aspectRatio(width : Int, height : Int) : Int {
let previewRatio = Math.max(width, height).toDouble() / Math.min(width, height)
if (Math.abs(previewRatio - this.RATIO_4_3_VALUE) <= Math.abs(previewRatio - this.RATIO_16_9_VALUE)) {
return AspectRatio.RATIO_4_3
}
return AspectRatio.RATIO_16_9
}
private imageToBitmap(image: Image): Bitmap {
// Assuming the image format is YUV_420_888 and converting it to Bitmap
let buffer = image.planes[0].buffer
let bytes = ByteArray(buffer.capacity())
buffer.get(bytes)
let yuvImage = YuvImage(bytes, ImageFormat.NV21, image.width, image.height, null)
let out = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0, image.width, image.height), 100, out)
let byteArray = out.toByteArray()
return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
}
private increaseBrightness(camera: Camera) {
// 调整曝光补偿以提高亮度
let cameraControl = camera.cameraControl
let cameraInfo = camera.cameraInfo
let exposureState = cameraInfo.exposureState
let range = exposureState.exposureCompensationRange
let maxExposure = range.upper
if (maxExposure > 0) {
cameraControl.setExposureCompensationIndex(maxExposure)
console.log("曝光补偿值已设置为最大值: $",maxExposure)
} else {
console.log("相机不支持曝光补偿。")
}
}
private setISO(camera: Camera) {
}
private setWhiteBalance(camera: Camera) {
}
private _openCamera() {
let _this = this;
let cameraProviderFuture = ProcessCameraProvider.getInstance(this.context)
class IntentRunable extends Runnable {
override run() {
let cameraProvider : ProcessCameraProvider = cameraProviderFuture.get()
let previewView = _this.cameraView! as PreviewView
let cameraSelector : CameraSelector = new CameraSelector.Builder()
.requireLensFacing(_this.CamerDeviceDir)
.build();
// 预览比例
let screenAspectRatio = _this.aspectRatio((300).toInt(), (300).toInt())
let preview : Preview = new Preview.Builder()
.setTargetAspectRatio(screenAspectRatio)
.build();
preview.setSurfaceProvider(previewView.getSurfaceProvider())
// 创建ImageCapture用例,并设置闪光灯模式
let imageCapture : ImageCapture = new ImageCapture.Builder()
.setTargetAspectRatio(screenAspectRatio)
.build();
let imageAnalysis = new ImageAnalysis.Builder()
.setTargetResolution(new Size(300, 300))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
class MyImageAnalyzer implements ImageAnalysis.Analyzer {
constructor() {
super()
}
override analyze(image : ImageProxy) {
// image
if (_this.isDecoderQring) {
_this.savePhotoTocache(image)
} else {
const imgInput = image.getImage()
if(imgInput!=null){
let img = InputImage.fromMediaImage(imgInput,image.getImageInfo().getRotationDegrees())
decodeImage((evt:XFACE_EVENT_SUCCESS[])=>{
UTSAndroid.getDispatcher("main").async(function(_){
let imgbitmap = image.toBitmap()
callVideoFaceFun(evt,imgbitmap)
image.close()
},null)
},()=>{},img)
}else{
image.close()
}
}
}
override getDefaultTargetResolution() : Size {
return new Size(_this.viewBox_width.toInt(), _this.viewBox_height.toInt());
}
}
let cameraExecutor = Executors.newSingleThreadExecutor();
imageAnalysis.setAnalyzer(cameraExecutor, new MyImageAnalyzer());
let recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.LOWEST)) // 选择录制质量,HD、FHD等
.build()
_this.videoCapture = VideoCapture.withOutput(recorder)
let camera : Camera = cameraProvider.bindToLifecycle(UTSAndroid.getUniActivity()! as LifecycleOwner,
cameraSelector,
imageCapture,
imageAnalysis,
preview,
videoCapture!
);
if (_this.takeModelType == 'video') {
// 录制视频
let filepath = _this.context.getExternalCacheDir()?.getPath() ?? ""
let videoFile = new File(filepath, 'tmui4xCamreaVideo' + getUid(1, 12) + '.mp4')
let outputOptions = FileOutputOptions.Builder(videoFile).build()
class DirectExecutor implements Executor {
override execute(r:Runnable ) {
r.run();
console.error('录制错误')
callVideoFun('')
}
}
// Consumer<VideoRecordEvent>
class reocdereventClass implements Consumer<VideoRecordEvent> {
override accept(event: VideoRecordEvent) {
if(event instanceof VideoRecordEvent.Start){
console.log("开始录制")
}else if(event instanceof VideoRecordEvent.Finalize){
console.log("结束录制",videoFile.absolutePath)
callVideoFun(videoFile.absolutePath)
}
}
}
// ContextCompat.getMainExecutor(_this.context)
_this.recoderVideoObj = _this.videoCapture!.getOutput()
.prepareRecording(_this.context,outputOptions)
.start(ContextCompat.getMainExecutor(_this.context),new reocdereventClass() as Consumer<VideoRecordEvent>)
}
let cameraControl = camera.getCameraControl()
let cameraInfo : CameraInfo = camera.getCameraInfo()
_this.increaseBrightness(camera)
// 闪光灯.
cameraControl.enableTorch(_this.flashMode)
_this.camera = camera
_this.cameraPreview = preview
_this.cameraProvider = cameraProvider
_this.isDecoderQring = false
_this.isOpeningCameraing = true
}
}
try{
cameraProviderFuture.addListener(new IntentRunable(), ContextCompat.getMainExecutor(this.context))
}catch(e){
console.error('开启错误.')
}
}
takPhoto(call : (path : string) => void) {
callFuntake = call;
this.isDecoderQring = true;
}
//front,前置,back后置
setCameraDir(type : string) {
this.CamerDeviceDir = type == 'back' ? CameraSelector.LENS_FACING_BACK : CameraSelector.LENS_FACING_FRONT
}
setFlash(type : boolean) {
this.flashMode = type;
}
savePhotoTocache(image : ImageProxy) {
let context = UTSAndroid.getAppContext()!
let filepath = context.getExternalCacheDir()?.getPath() ?? ""
let img : Bitmap | null = image.toBitmap()
let file = new File(filepath, 'tmui4xCamrea' + getUid(1, 12) + '.jpg');
let fos = new FileOutputStream(file)
let conunt = (img?.getByteCount() ?? 0) / 1024 / 1024
let saved = img!.compress(Bitmap.CompressFormat.JPEG, conunt > 1 ? 64 : 100, fos);
let t = this;
class IntentRunable2 extends Runnable {
override run() {
if (saved) {
callFuntake(file.getPath())
t.isDecoderQring = false
}
// ,拍照暂不关闭相机让用户手动关闭.
// t.closeCamera()
}
}
image.close()
new Handler(Looper.getMainLooper()).post(new IntentRunable2());
}
savePhotoBitmapTocache(img : Bitmap) {
let context = UTSAndroid.getAppContext()!
let filepath = context.getExternalCacheDir()?.getPath() ?? ""
let file = new File(filepath, 'tmui4xCamrea' + getUid(1, 12) + '.jpg');
let fos = new FileOutputStream(file)
let conunt = img.getByteCount() / 1024 / 1024
img!.compress(Bitmap.CompressFormat.JPEG, conunt > 1 ? 64 : 100, fos);
return file.getPath();
}
}
@@ -0,0 +1,12 @@
{
"minSdkVersion": "21",
"dependencies":[
"com.google.mlkit:face-detection:16.1.7",
"androidx.camera:camera-core:1.3.4",
"androidx.camera:camera-camera2:1.3.4",
"androidx.camera:camera-lifecycle:1.3.4",
"androidx.camera:camera-video:1.3.4",
"androidx.camera:camera-view:1.3.4",
"androidx.camera:camera-extensions:1.3.4"
]
}
@@ -0,0 +1,561 @@
import Intent from 'android.content.Intent';
import Context from 'android.content.Context'
import IOException from 'java.io.IOException'
import InputStream from 'java.io.InputStream'
import Uri from 'android.net.Uri';
import File from 'java.io.File'
import Bitmap from "android.graphics.Bitmap"
import FrameLayout from 'android.widget.FrameLayout';
import FaceDetection from "com.google.mlkit.vision.face.FaceDetection"
import FaceDetectorOptions from "com.google.mlkit.vision.face.FaceDetectorOptions"
import FaceLandmark from "com.google.mlkit.vision.face.FaceLandmark"
import FaceContour from "com.google.mlkit.vision.face.FaceContour"
import Face from "com.google.mlkit.vision.face.Face"
import InputImage from "com.google.mlkit.vision.common.InputImage"
import Task from "com.google.android.gms.tasks.Task"
import List from "java.util.List"
import ByteBuffer from 'java.nio.ByteBuffer'
import ImageFormat from 'android.graphics.ImageFormat'
import Kotlin from 'kotlin.jvm.internal.Intrinsics.Kotlin';
import Image from 'android.media.Image';
import LinearLayout from 'android.widget.LinearLayout';
import ViewGroup from 'android.view.ViewGroup';
import { XFACE_CHECK_OPTS, XFACE_EVENT_SUCCESS, XFACE_TEST_EYE_TYPE, XFACE_FACE_Direction, XFACE_EVENT_CHECK_SUCCESS } from "../interface.uts"
import { XFACE_CHECK_OPTS_REAL, getDefaultConfig } from "../libs/config.uts"
import BitmapFactory from 'android.graphics.BitmapFactory';
import View from 'android.view.View';
import { xCamera } from "./camera.uts"
import { xAudio } from "./audio.uts"
let camera : xCamera | null = null;
const audio = new xAudio();
const checkFaceResult = (image : InputImage, faces : MutableList<Face>, callBack : (result : XFACE_EVENT_SUCCESS[]) => void) => {
const cfaces = [] as XFACE_EVENT_SUCCESS[]
// @ts-ignore
for (item in faces) {
// @ts-ignore
const face = item as Face;
let rightEye = { openNum: 0, isOpen: false } as XFACE_TEST_EYE_TYPE;
let leftEye = { openNum: 0, isOpen: false } as XFACE_TEST_EYE_TYPE;
// 眼睛
let rightEyeMark = face.getLandmark(FaceLandmark.RIGHT_EYE)
let leftEyeMark = face.getLandmark(FaceLandmark.RIGHT_EYE)
// 鼻子
let noseMark = face.getLandmark(FaceLandmark.NOSE_BASE)
// 耳朵
let rightEarMark = face.getLandmark(FaceLandmark.RIGHT_EAR)
let leftEarMark = face.getLandmark(FaceLandmark.LEFT_EAR)
let rightEyeNum = face.getRightEyeOpenProbability()
let leftEyeNum = face.getLeftEyeOpenProbability()
let isSmile = face.getSmilingProbability()
let faceId = face.getTrackingId()
if (rightEyeNum != null) {
rightEye.openNum = rightEyeNum + 0;
rightEye.isOpen = rightEyeNum > 0.7
}
if (leftEyeNum != null) {
leftEye.openNum = leftEyeNum + 0;
leftEye.isOpen = leftEyeNum > 0.7
}
// 检测当前左,右,下,上
let faceRightLeft : XFACE_FACE_Direction = "center"
let faceUpDown : XFACE_FACE_Direction = "center"
let eulerX = face.getHeadEulerAngleX(); // 获取头部左右旋转角度
let eulerY = face.getHeadEulerAngleY(); // 获取头部上下俯仰角度
let eulerZ = face.getHeadEulerAngleZ(); // 获取头部上下俯仰角度
// 嘴巴
let leftMouth = face.getLandmark(FaceLandmark.MOUTH_LEFT)
let rightMouth = face.getLandmark(FaceLandmark.MOUTH_RIGHT)
let bottomMouth = face.getLandmark(FaceLandmark.MOUTH_BOTTOM)
let isMouthOpened = false
let ANGLE_THRESHOLD = 30; // 容差值
let ANGLE_HEAD_THRESHOLD = 0; // 容差值
let ANGLE_EYE_THRESHOLD = 6; // 眨眼的容差.
let allHeadBody = false;
if (Math.abs(eulerY) > ANGLE_THRESHOLD) { // 确保头部没有明显上下俯仰
faceRightLeft = eulerY > 0 ? 'left' : 'right';
}
if (Math.abs(eulerX) >= ANGLE_HEAD_THRESHOLD) { // 确保头部没有明显上下俯仰
faceUpDown = eulerY > 0 ? 'down' : 'up';
}
if (leftMouth != null && rightMouth != null && bottomMouth != null) {
// @ts-ignore
let mouthWidth = rightMouth.position.x - leftMouth.position.x
// @ts-ignore
let mouthHeight = bottomMouth.position.y - ((leftMouth.position.y + rightMouth.position.y) / 2)
if (mouthHeight / mouthWidth > 0.4 && faceRightLeft == 'center') {
isMouthOpened = true;
} else {
isMouthOpened = false;
}
}
if(
leftMouth != null &&
rightMouth != null &&
bottomMouth != null &&
noseMark !=null&&
rightEarMark !=null&&
leftEarMark !=null&&
rightEyeMark !=null&&
leftEyeMark !=null
){
allHeadBody = true;
}
cfaces.push({
imgWidth: image.getWidth(),
imgHeight: image.getHeight(),
isSmile: isSmile == null ? false : (isSmile >= 0.8),
rightEye,
leftEye,
faceRightLeft,
faceUpDown,
isMouthOpened,
allHeadBody,
faceId:faceId!=null?(faceId+0):null
})
}
callBack(cfaces)
}
export function decodeImage(success : (evt : XFACE_EVENT_SUCCESS[]) => void, fail : () => void, image : InputImage) {
// @ts-ignore
const highAccuracyOpts = new FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
.setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
.setContourMode(FaceDetectorOptions.CONTOUR_MODE_NONE)
.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
.enableTracking()
.build()
const detector = FaceDetection.getClient(highAccuracyOpts)
detector.process(image)
.addOnSuccessListener((faces : MutableList<Face>) => {
UTSAndroid.getDispatcher("main").async(function(_){
checkFaceResult(image, faces, (result : XFACE_EVENT_SUCCESS[]) => {
success(result)
})
},null)
})
.addOnFailureListener(e => {
console.error(e)
fail()
})
}
/**
* 通过相册选择检测当前图片是否有人脸.
*/
export function checkFaceByImage(config : XFACE_CHECK_OPTS | null = null) {
const cfg = getDefaultConfig(config)
// @ts-ignore
const highAccuracyOpts = new FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
.setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL)
.setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
.build()
// @ts-ignore
uni.chooseImage({
count: 1,
// @ts-ignore
success(evt : ChooseImageSuccess) {
if (evt.tempFilePaths.length == 0) return;
const imgs = evt.tempFilePaths[0] as string;
const decodedPath = UTSAndroid.convert2AbsFullPath(imgs)
const file = new File(decodedPath)
const image = InputImage.fromFilePath(UTSAndroid.getAppContext() as Context, Uri.fromFile(file));
// const image = InputImage.fromBitmap(BitmapFactory.decodeFile(decodedPath)!, 0)
const detector = FaceDetection.getClient(highAccuracyOpts)
detector.process(image)
.addOnSuccessListener((faces : MutableList<Face>) => {
if (faces.size == 0) {
cfg.success({
isPass: false,
images: [decodedPath] as string[],
videoPath: ""
})
return;
}
checkFaceResult(image, faces, (result : XFACE_EVENT_SUCCESS[]) => {
cfg.enter(result,JSON.stringify(result))
cfg.success({
isPass: true,
images: [decodedPath] as string[],
videoPath: ""
})
})
})
.addOnFailureListener(e => {
console.error(e)
cfg.fail("解析错误")
})
},
fail() {
console.error("未选择图片")
cfg.fail("未选择图片")
}
})
}
/**
* 提供外部图片地址检测当前图片是否存在人脸
*/
export function checkFaceByImageFromFilePath(config : XFACE_CHECK_OPTS) {
const cfg = getDefaultConfig(config)
if (cfg.url == '') {
cfg.fail("url图片地址没提供正确.")
return;
}
const decodedPath = UTSAndroid.convert2AbsFullPath(cfg.url)
const file = new File(decodedPath)
const image = InputImage.fromFilePath(UTSAndroid.getAppContext() as Context, Uri.fromFile(file));
// const image = InputImage.fromBitmap(BitmapFactory.decodeFile(decodedPath)!, 0)
// @ts-ignore
const highAccuracyOpts = new FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
.setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
.build()
const detector = FaceDetection.getClient(highAccuracyOpts)
detector.process(image)
.addOnSuccessListener((faces : MutableList<Face>) => {
if (faces.size == 0) {
cfg.enter([] as XFACE_EVENT_SUCCESS[],JSON.stringify([] as XFACE_EVENT_SUCCESS[] ))
cfg.fail("未检测到人脸")
return;
}
checkFaceResult(image, faces, (result : XFACE_EVENT_SUCCESS[]) => {
cfg.enter(result,JSON.stringify(result))
cfg.success({
isPass: true,
images: [decodedPath] as string[],
videoPath: ""
})
})
})
.addOnFailureListener(e => {
console.error(e)
cfg.fail("解析错误")
})
}
/**
* 检查相机权限
*/
export function checkCamraPermissions(call : (istrue : boolean) => void) {
let permissionCheck = ["android.permission.CAMERA"]
if (UTSAndroid.checkSystemPermissionGranted(UTSAndroid.getUniActivity()!, permissionCheck)) {
call(true)
} else {
console.log("当前不具备指定权限")
// 请求拍照权限
UTSAndroid.requestSystemPermission(UTSAndroid.getUniActivity()!, permissionCheck, function (allRight : boolean, _ : string[]) {
call(true)
}, function (_ : boolean, _ : string[]) {
//用户拒绝了部分权限
call(false)
})
}
}
let tid = 0
//当前等待检测的指令
let nowInsIndex = 0;
//是否允许开始动作检测
//allow,next,wait'
let isCheckNext = 'wait'
//指令执行中start,checking,dercoding,end
let actioning = ''
/**
* 打开视频进行人脸活体检测
*/
@UTSJS.keepAlive
export function checkFaceByCamra(config : XFACE_CHECK_OPTS) {
if (camera != null) {
camera!.closeCamera()
clearTimeout(tid)
}
actioning = ''
isCheckNext = 'wait'
nowInsIndex = 0
const cfg = getDefaultConfig(config)
if (cfg.viewId == '') {
cfg.fail("没有提供正在的view的id绑定.")
return;
}
if (cfg.instruction.length == 0) {
console.warn("未提供指令,将仅进行人脸验证.")
// cfg.fail("没有提供活体检测指令")
// return;
}
// @ts-ignore
let ele = uni.getElementById(cfg.viewId) as UniElement | null;
if (ele == null) {
cfg.fail("绑定的容器view找不到")
return;
}
let parentView = ele.getAndroidView() as ViewGroup;
let viewWidth = parentView.getWidth()
let viewHeight = parentView.getHeight()
let grop = new FrameLayout(UTSAndroid!.getAppContext()!)
let layaout = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
grop.setLayoutParams(layaout)
parentView.addView(grop)
let isPassCheck = false
let checkImgs = [] as string[]
let lastFaceAllBody = ""
let faceId = null as null|number;
if (viewWidth == 0 || viewHeight == 0) {
cfg.fail("绑定的容器view宽高不能为0")
return;
}
camera = new xCamera(grop)
camera?.setCameraDir('front')
camera?.setFlash(false)
camera?.startRecoderVideo((videoPath : string) => {
console.log(videoPath)
UTSAndroid.getDispatcher("main").async(function (_) {
if(lastFaceAllBody!=''){
checkImgs.push(lastFaceAllBody)
}
cfg.success({
videoPath,
images:checkImgs,
isPass:isPassCheck
} as XFACE_EVENT_CHECK_SUCCESS)
})
})
function closeCamera() {
camera?.stopRecoderVideo()
camera?.closeCamera()
parentView.removeAllViews()
}
tid = setTimeout(() => {
cfg.insTipsEvent("start")
actioning = 'start'
isCheckNext = 'allow'
}, 2000)
let headDonwUp = 0
UTSAndroid.getDispatcher("main").async(function (_) {
camera?.setCheckFaceEnter((result:XFACE_EVENT_SUCCESS[],imgBit:any|null) => {
// 无指令时的正脸检测
if(cfg.instruction.length==0){
if(result.length>0){
let item = result[0]
if(item.allHeadBody){
nowInsIndex = -1;
if(imgBit!=null&&camera!=null&&imgBit instanceof Bitmap){
checkImgs.push(camera!.savePhotoBitmapTocache((imgBit! as Bitmap)))
}
isPassCheck = true;
closeCamera()
}
}
return;
}
if (nowInsIndex == -1 || actioning == '') {
return;
}
if (nowInsIndex > cfg.instruction.length - 1) {
nowInsIndex = -1;
cfg.insTipsEvent("end")
isPassCheck = true;
closeCamera()
return;
}
let item = null as null|XFACE_EVENT_SUCCESS
if(result.length>0){
item = result[0]
}
let dir = cfg.instruction[nowInsIndex]
if (actioning == 'start') {
actioning = ''
audio.play(dir)
setTimeout(function () {
cfg.insTipsEvent("ins_start_" + dir)
isCheckNext = 'allow'
actioning = 'checking'
}, 1500);
return;
}
// 保存一次正脸
if(lastFaceAllBody==""){
if(result.length>0){
let item = result[0]
if(item.allHeadBody){
lastFaceAllBody = camera!.savePhotoBitmapTocache(imgBit! as Bitmap)
faceId = item.faceId
console.log(faceId,'+++')
}
}
return;
}
// 进行人脸检测
if(item!=null&&actioning == 'checking'&&isCheckNext == 'allow'){
let rts = result!
if(faceId!=item.faceId||item.faceId==null||faceId==null){
console.log(item.faceId,'---')
isPassCheck = false;
console.log("检脸追踪id变化,检测失败")
nowInsIndex = -1;
cfg.insTipsEvent("end")
closeCamera()
return;
}
// 向左转
if (dir == 'left' && item.faceRightLeft == 'left') {
// 立即更改状态,防止重复处理
actioning = 'dercoding'
cfg.insTipsEvent("ins_end_" + dir)
cfg.enter(result,JSON.stringify(rts))
isCheckNext = 'wait';
audio.play('goold')
if(imgBit!=null&&camera!=null){
checkImgs.push(camera!.savePhotoBitmapTocache(imgBit! as Bitmap))
}
tid = setTimeout(function () {
isCheckNext = 'next';
}, 1000);
} else if (dir == 'right' && item.faceRightLeft == 'right') {
// 立即更改状态,防止重复处理
actioning = 'dercoding'
cfg.insTipsEvent("ins_end_" + dir)
cfg.enter(result,JSON.stringify(rts))
isCheckNext = 'wait';
audio.play('goold')
if(imgBit!=null&&camera!=null){
checkImgs.push(camera!.savePhotoBitmapTocache(imgBit! as Bitmap))
}
tid = setTimeout(function () {
isCheckNext = 'next';
}, 1000);
} else if(dir == 'downUp'&&(item.faceUpDown=='up'||item.faceUpDown=='down')){
if(item.faceUpDown=='up'&&headDonwUp==0){
headDonwUp = 1;
}else if(item.faceUpDown=='down'&&headDonwUp==1){
headDonwUp = 2;
// 立即更改状态,防止重复处理
actioning = 'dercoding'
cfg.insTipsEvent("ins_end_" + dir)
cfg.enter(result,JSON.stringify(rts))
audio.play('goold')
isCheckNext = 'wait';
if(imgBit!=null&&camera!=null){
checkImgs.push(camera!.savePhotoBitmapTocache(imgBit! as Bitmap))
}
tid = setTimeout(function () {
isCheckNext = 'next';
headDonwUp = 0
}, 1000);
}
}else if(dir == 'monthOpenClose'){
if(item.isMouthOpened==true&&headDonwUp==0){
headDonwUp = 1;
}else if(item.isMouthOpened == false&&headDonwUp==1){
headDonwUp = 2;
// 立即更改状态,防止重复处理
actioning = 'dercoding'
cfg.insTipsEvent("ins_end_" + dir)
cfg.enter(result,JSON.stringify(rts))
audio.play('goold')
isCheckNext = 'wait';
if(imgBit!=null&&camera!=null){
checkImgs.push(camera!.savePhotoBitmapTocache(imgBit! as Bitmap))
}
tid = setTimeout(function () {
isCheckNext = 'next';
headDonwUp = 0
}, 1000);
}
}else if(dir == 'eyeOplenClose'){
if((item.leftEye.isOpen&&item.rightEye.isOpen)&&headDonwUp==0){
headDonwUp = 1;
}else if((!item.leftEye.isOpen&&!item.rightEye.isOpen)&&headDonwUp==1){
headDonwUp = 2;
}else if((item.leftEye.isOpen&&item.rightEye.isOpen)&&headDonwUp==2){
headDonwUp = 3;
// 立即更改状态,防止重复处理
actioning = 'dercoding'
cfg.insTipsEvent("ins_end_" + dir)
cfg.enter(result,JSON.stringify(rts))
audio.play('goold')
isCheckNext = 'wait';
if(imgBit!=null&&camera!=null){
checkImgs.push(camera!.savePhotoBitmapTocache(imgBit! as Bitmap))
}
tid = setTimeout(function () {
isCheckNext = 'next';
headDonwUp = 0
}, 1000);
}
}
}
if (isCheckNext == 'next'){
isCheckNext = 'allow';
actioning = 'start'
nowInsIndex += 1;
}
})
})
}