美文网首页
微信小程序使用camera + wx.faceDetect 人脸

微信小程序使用camera + wx.faceDetect 人脸

作者: pjqdyd | 来源:发表于2021-08-20 16:47 被阅读0次

    写在前面:

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

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

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

    录制思路:

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

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

    需要注意的是:

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

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

    录像组件核心代码:

    <!--components/camera-face/index.wxml-->
    <!--人脸识别录像组件-->
    <view class="page-body">
        <view class="camera-box">
            <camera
                mode="normal"
                device-position="{{devicePosition}}"
                frame-size="{{frameSize}}"
                resolution="{{resolution}}"
                flash="{{flash}}"
                binderror="error"
                bindstop="stop"
                class="camera">
            </camera>
            <view class="img-view">
                <image mode="scaleToFill" class="mask-img" src="../../static/images/mask.png"></image>
            </view>
        </view>
        <view class="bottom-view">
            <view wx:if="{{!bottomTips}}" class="bottom-btn" bindtap="readyRecord">准备录制人脸</view>
            <view wx:else class="bottom-tips">{{bottomTips}}</view>
        </view>
    </view>
    
    
    // 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.wxml -->
    <view class="page">
      <!-- 父元素一定要有高度 -->
      <camera-face id="cameraFace" bind:noAuth="handleNoAuth" bind:complete="handleComplete" bind:cannotUse="handleCannotuse" />
      
      <view class="preview-tips">预览视频</view>
      <video wx:if="{{videoSrc}}" class="video" src="{{videoSrc}}"></video>
    </view>
    
    // 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. 自定义的<camera-face>组件向外触发了 noAuth用户未授权摄像录音、cannotUse不可使用、complete录制完成事件,你也可以自定义修改组件触发更多的事件。

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

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

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

    相关文章

      网友评论

          本文标题:微信小程序使用camera + wx.faceDetect 人脸

          本文链接:https://www.haomeiwen.com/subject/wdytiltx.html