微信小程序使用camera + wx.faceDetect 人脸识别录制人脸视频

写在前面:

注意:小程序官方不支持采集人脸等用户隐私信息,采集用户信息建议使用官方提供的人脸核身接口,其他任何第三方的都审核不过。人脸核身需要一些资质(部分类目的小程序)。

参考官方说明:
1.微信人脸核身接口能力说明
2. 腾讯人脸核身SDK接入

本案例是另外一种实现方式,使用了组件和wx.faceDetect小程序API,只是作代码演示,并不建议在生产中使用,因为可能过不了审核。

录制思路:

  1. 使用小程序的 组件 和 CameraContext.startRecord等接口开启摄像头录制。

  2. 使用wx.faceDetect()人脸识别接口对摄像头的视频流帧进行识别(检测是否是人脸且是正脸)。

需要注意的是:

  1. 用户是否授权摄像头和录音。
  2. 用户的微信版本是否可以调用wx.faceDetect接口(基础库:2.18.0)。
  3. 用户人脸移出/不是正脸取消录制,并在正脸时重新录制。
  4. 准备录制-录制中-录制完成几种状态文案切换,还有一句录制中倒计时提示。
  5. 视频帧检测调用函数节流,防止调用wx.faceDetect过于频繁引起卡顿(影响识别到人脸的时间)。
  6. 开发者工具开启增强编译,修改成你的appid,且需要真机预览,调试和在pc模拟器中会报错。

截图案例 (非真实截图):

录像组件核心代码:




    
        
        
        
            
        
    
    
        准备录制人脸
        {{bottomTips}}
    


// components/camera-face/index.js

import { getAuthorize, setAuthorize, throttle, checkVersion } from './utils'

// 提示信息
const tips = {
  ready: '请确保光线充足,正面镜头',
  recording: '人脸录制中..',
  complete: '已录制完成',
  error: '录制失败'
}

Component({

  // 组件的属性列表
  properties: {
    // 人脸整体可信度 [0-1], 参考wx.faceDetect文档的res.confArray.global
    // 当超过这个可信度且正脸时开始录制人脸, 反之停止录制
    faceCredibility: {
      type: Number,
      value: 0.5
    },
    // 人脸偏移角度正脸数值参考wx.faceDetect文档的res.angleArray
    // 越接近0越正脸,包括p仰俯角(pitch点头), y偏航角(yaw摇头), r翻滚角(roll左右倾)
    faceAngle: {
      type: Object,
      value: { p: 0.5, y: 0.5, r: 0.5 }
    },
    // 录制视频时长,不能超过30s
    duration: {
      type: Number,
      value: 3000
    },
    // 是否压缩视频
    compressed: {
      type: Boolean,
      value: false
    },
    // 前置或者后置 front,back
    devicePosition: {
      type: String,
      value: 'front'
    },
    // 指定期望的相机帧数据尺寸 small,medium,large
    frameSize: {
      type: String,
      value: 'medium'
    },
    // 分辨率 low,medium,high
    resolution: {
      type: String,
      value: 'medium'
    },
    // 闪光灯 auto,on,off,torch
    flash: {
      type: String,
      value: 'off'
    },
    // 检测视频帧的节流时间,默认500毫秒执行一次
    throttleFrequency: {
      type: Number,
      value: 500
    }
  },

  // 组件页面的生命周期
  pageLifetimes: {
    // 页面被隐藏
    hide: function() {
      this.stop()
    },
  },
  detached: function() {
    // 在组件实例被从页面节点树移除时执行
    this.stop()
  },

  // 组件的初始数据
  data: {
    isReading: false, // 是否在准备中
    isRecoding: false, // 是否正在录制中
    isStopRecoding: false, // 是否正在停止录制中
    bottomTips: '', // 底部提示文字
  },

  /**
   * 组件的方法列表
   */
  methods: {

    // 开启相机ctx
    async start() {
      const result = await this.initAuthorize();
      if (!result) return false;
      if (!this.ctx) this.ctx = wx.createCameraContext();
      return true;
    },

    // 准备录制
    async readyRecord() {
      if (this.data.isReading) return
      this.setData({ isReading: true })
      wx.showLoading({ title: '加载中..', mask: true })
      // 检测版本号
      const canUse = checkVersion('2.18.0', () => {
        this.triggerEvent('cannotUse')
      })
      if (!canUse) {
        wx.hideLoading()
        this.setData({ isReading: false })
        return
      }

      // 启用相机
      try {
        const result = await this.start()
        if (!result || !this.ctx) throw new Error()
      } catch (e) {
        wx.hideLoading()
        this.setData({ isReading: false })
        return
      }
      console.log('准备录制')
      this.setData({ bottomTips: tips.ready })
      // 视频帧回调节流函数
      let fn = throttle((frame) => {
        // 人脸识别
        wx.faceDetect({
          frameBuffer: frame.data,
          width: frame.width,
          height: frame.height,
          enableConf: true,
          enableAngle: true,
          success: (res) => this.processFaceData(res),
          fail: (err) => this.cancel()
        })
      }, this.properties.throttleFrequency);

      // 初始化人脸识别
      wx.initFaceDetect({
        success: () => {
          const listener = this.listener = this.ctx.onCameraFrame((frame) => fn(frame));
          listener.start();
        },
        fail: (err) => {
          console.log('初始人脸识别失败', err)
          this.setData({ bottomTips: '' })
          wx.showToast({ title: '初始人脸识别失败', icon: 'none' })
        },
        complete: () => {
          wx.hideLoading()
          this.setData({ isReading: false })
        }
      })
    },

    // 处理人脸识别数据
    processFaceData(res) {
      if(res.confArray && res.angleArray) {
        const { global } = res.confArray;
        const g = this.properties.faceCredibility;
        const { pitch, yaw, roll } = res.angleArray;
        const { p, y, r } = this.properties.faceAngle;
        console.log('res.confArray.global:', global)
        console.log('res.angleArray:',  pitch, yaw, roll)
        const isGlobal = global >= g;
        const isPitch = Math.abs(pitch) <= p;
        const isYaw = Math.abs(yaw) <= y;
        const isRoll = Math.abs(roll) <= r;
        if( isGlobal && isPitch && isYaw && isRoll ){
          console.log('人脸可信,且是正脸');
          if (this.data.isRecoding || this.data.isCompleteRecoding) return
          this.setData({ isRecoding: true });
          this.startRecord(); // 开始录制
        }else {
          console.log('人脸不可信,或者不是正脸');
          this.cancel()
        }
      }else {
        console.log('获取人脸识别数据失败', res);
        this.cancel()
      }
    },

    // 开始录制
    startRecord() {
      console.log('开始录制')
      this.ctx.startRecord({
        success: (res) => {
          this.setRecordingTips();
          this.timer = setTimeout(() => {
            this.completeRecord()
          }, this.properties.duration)
        },
        timeoutCallback: (res) => {
          // 超过30s或页面 onHide 时会结束录像
          this.stop();
        },
        fail: () => this.stop()
      })
    },
    // 设置录制中的提示文字和倒计时
    setRecordingTips() {
      let second = (this.properties.duration / 1000);
      if (this.interval) clearInterval(this.interval);
      this.interval = setInterval(() => {
        console.log('xxxxxx', second);
        this.setData({
          bottomTips: tips.recording + second-- + 's'
        })
        if (second <= 0) clearInterval(this.interval);
      }, 1000)
    },

    // 完成录制
    completeRecord() {
      console.log('完成录制');
      this.setData({ isCompleteRecoding: true })
      this.ctx.stopRecord({
        compressed: this.properties.compressed,
        success: (res) => {
          this.setData({ bottomTips: tips.complete })
          // 向外触发完成录制的事件
          this.triggerEvent('complete', res.tempVideoPath)
        },
        fail: () => this.stop(),
        complete: () => {
          this.listener.stop();
          wx.stopFaceDetect();
          clearInterval(this.interval);
          this.setData({ isCompleteRecoding: false })
        }
      })
    },
    // 人脸移出等取消录制
    cancel() {
      console.log('取消录制');
      // 如果不在录制中或者正在录制完成中就不能取消
      if (!this.data.isRecoding || this.data.isCompleteRecoding) return
      clearTimeout(this.timer);
      clearInterval(this.interval);
      this.ctx.stopRecord({
        complete: () => {
          console.log('取消录制成功');
          this.setData({ bottomTips: tips.ready, isRecoding: false });
        }
      });
    },
    // 用户切入后台等停止使用摄像头
    stop() {
      console.log('停止录制');
      clearTimeout(this.timer);
      clearInterval(this.interval);
      if(this.listener) this.listener.stop();
      if (this.ctx && !this.data.isCompleteRecoding) this.ctx.stopRecord()
      wx.stopFaceDetect();
      setTimeout(() => {
        this.setData({ bottomTips: '', isRecoding: false })
      }, 500)
    },
    // 用户不允许使用摄像头
    error(e) {
      // const cameraName = 'scope.camera';
      // this.triggerEvent('noAuth', cameraName)
    },

    // 初始相机和录音权限
    async initAuthorize() {
      const cameraName = 'scope.camera';
      const recordName = 'scope.record';
      const scopeCamera = await getAuthorize(cameraName);
      // 未授权相机
      if (!scopeCamera) {
        // 用户拒绝授权相机
        if (!(await setAuthorize(cameraName))) this.openSetting();
        return false;
      }
      const scopeRecord = await getAuthorize(recordName);
      if (!scopeRecord) {
        // 用户拒绝授权录音
        if (!(await setAuthorize(recordName))) {
          this.openSetting();
          return false;
        }
      }
      return true;
    },

    // 打开设置授权
    openSetting() {
      wx.showModal({
        title: '开启摄像头和录音权限',
        showCancel: true,
        content: '是否打开?',
        success: (res) => {
          this.triggerEvent('noAuth', '打开设置授权')
          if (res.confirm) {
            wx.openSetting();
          }
        }
      });
    }
  }
})

页面使用核心代码:



  
  
  
  预览视频
  

// pages/page2/index.js
Page({

  onHide() {
    // 在录制中退出后台页面隐藏,返回上一页,确保重新进入当前页
    // 防止在录制中退出后台导致下次重新录制失败 "operateCamera:fail:is stopping"
    console.log('页面隐藏')
    if (this.data.isBack) wx.navigateBack()
  },

  onShow() {
    console.log('页面显示')
    this.setData({ isBack: true })
  },

  data: {
    videoSrc: '', // 录制的视频临时路径
    isBack: false // 是否返回上一页,用于页面隐藏时判断
  },

  // 当取消授权或者打开设置授权
  handleNoAuth(res) {
    console.log("用户拒绝授权:", res);
    // 因为在设置里授权摄像头不会立即生效,所以要返回上一页,确保重新进入当前页使摄像头生效
    setTimeout(() => {
      wx.navigateBack()
    }, 500)
  },

  // 版本号过低的回调
  handleCannotuse() {
    console.log('版本号过低无法使用, 组件内已经弹窗提示过了');
    wx.navigateBack()
  },

  // 视频录制完成
  handleComplete(e) {
    console.log('视频文件路径:', e.detail)
    // e.detail: 视频临时路径
    this.setData({ videoSrc: e.detail, isBack: false })

    // 打印视频信息文件
    wx.getFileInfo({
      filePath: e.detail,
      success: (res) => {
        const { size } = res
        console.log("视频文件大小M:", size / Math.pow(1024, 2));
      },
      fail: (err) => {
        console.log("获取视频文件失败", err);
      }
    })
  }
})

完整代码示例:github仓库

总结:

  1. 自定义的组件向外触发了 noAuth用户未授权摄像录音、cannotUse不可使用、complete录制完成事件,你也可以自定义修改组件触发更多的事件。

  2. wx.faceDetect只是进行人脸匹配检测,目前(2021.11)没有活体检测/身份识别功能,如果需要,简单的活体检测可以自己写写摇头抬头;或者更靠谱的上传人脸视频到后端处理/直接采用第三方的活体检测/身份识别接口功能。

  3. wx.faceDetect是否存在平台兼容/手机差异问题?目前没发现,但社区有人遇到过问题,需要详细测试。

  4. 如果代码发现可优化,欢迎提issue改进。

你可能感兴趣的:(微信小程序使用camera + wx.faceDetect 人脸识别录制人脸视频)