媒体流数据获取和人脸检测

33次阅读

共计 6008 个字符,预计需要花费 16 分钟才能阅读完成。

背景

由于最近这些年互联网面向的受众面越来越光, 有了更多的人和设备访问互联网, 这也就意味着我们要做的就越来越多, 不在是像以前那样只需要调整页面样式和切图就可以了。所以就有了这篇文章的诞生,本次讲的如何在本地唤醒相机然后进行拍照,同时对拍照的照片进行修改,还可以对摄像头所捕捉的数据进行一个转发,同时还可以检测摄像头中是否存在人。

关键字 API 以及兼容性

唤醒摄像头主要用的 API 是 navigator.mediaDevices.getUserMedia。入参是一个对象 {audio: true, video: true} 是判断是否启动摄像头和录音,返回一个 promise 对象。以前的 navigator.getUserMedia 已经被废弃。如果 video 没有设置自动播放 需要在成功回调函数那里手动调用执行,只有这样摄像头才会被执行,不然就不会正确显示。

目前 IOS 设备的微信和 Safari 均不支持,较新的安卓和桌面端浏览器均支持。另外,出于安全问题考虑,Chrome 只支持 HTTPS 页面启用摄像头。

开启本地相机

test 是用 vue 来的。因为考虑到到时候直接复制就好,所以在用的时候需要考虑自己的环境来进行部分修改和调整

  HTML 部分
  
  <div class="contentarea">
    <div class="camera">
      <video ref="video" autoplay muted @canplay="canplay"></video>
      <button ref="startbutton" id="startbutton" @click="takepicture">Take photo</button>
    </div>
    <canvas ref="canvas"></canvas>
    <div class="output">
      <img ref="photo">
    </div>
  </div>
  
  CSS 部分
  
    #photo {
      border: 1px solid black;
      width: 320px;
      height: 240px;
    }
    
    #canvas {display: none;}
    
    .camera {
      width: 340px;
      display: inline-block;
    }
    
    .output {
      width: 340px;
      display: inline-block;
    }
    
    #startbutton {
      display: block;
      position: relative;
      margin-left: auto;
      margin-right: auto;
      bottom: 32px;
      background-color: rgba(0, 150, 0, 0.5);
      border: 1px solid rgba(255, 255, 255, 0.7);
      box-shadow: 0px 0px 1px 2px rgba(0, 0, 0, 0.2);
      font-size: 14px;
      font-family: "Lucida Grande", "Arial", sans-serif;
      color: rgba(255, 255, 255, 1);
    }
    
    .contentarea {
      font-size: 16px;
      font-family: "Lucida Grande", "Arial", sans-serif;
      width: 760px;
    }
    
    JS 部分
    
    // 监听摄像头是否启动成功并且按照 4:3 的比例初始化摄像头和画布
    canplay () {let { video, canvas} = this.$refs
      if (!this.streaming) {this.height = video.videoHeight / (video.videoWidth / this.width);
        console.log(this.height)
        if (isNaN(this.height)) {this.height = this.width / (4 / 3);
        }
        video.setAttribute("width", this.width);
        video.setAttribute("height", this.height);
        canvas.setAttribute("width", this.width);
        canvas.setAttribute("height", this.height);
        this.streaming = true;
      }
    },
    // 获取摄像头被点击的那一刻的数据并且存到 canvas 里面以便进行修改 然后在存储在 img 标签显示在页面
    takepicture () {let { video, canvas, photo} = this.$refs
      let context = canvas.getContext("2d")
      if (this.width && this.height) {
        canvas.width = this.width;
        canvas.height = this.height;
        context.drawImage(video, 0, 0, this.width, this.height);
        let data = canvas.toDataURL("image/png");
        photo.setAttribute("src", data);
      } else {this.clearphoto();
      }
    },
    // 清除 canvas 信息
    clearphoto () {let { canvas, photo} = this.$refs
      var context = canvas.getContext("2d");
      context.fillStyle = "#AAA";
      context.fillRect(0, 0, canvas.width, canvas.height);

      var data = canvas.toDataURL("image/png");
      photo.setAttribute("src", data);
    },
    // 初始化摄像头调用
    startup() {let { video} = this.$refs
      this.width = 320
      this.height = 0
      this.streaming = false
      navigator.mediaDevices
        .getUserMedia({video: true, audio: false})
        .then((stream) => {
          video.srcObject = stream;
          video.play();})
        .catch((err) => {console.log("An error occurred:" + err);
        })
    }

autoplay 需要开启自动播放不然需要在成功的回调里面手动执行,不然不会显示内容. 如果只是开启摄像头部分我们不需要声音,所以关闭掉了

 <video ref="video" autoplay muted @canplay="canplay"></video>
 

我们需要等页面加载之后手动开启摄像头方法。

mounted() {this.startup()
}

startup() {let { video} = this.$refs // 获取页面上面的视频元素
  this.width = 320 // 初始化宽度
  this.height = 0 // 初始化高度
  this.streaming = false // 初始化是否进行中
  navigator.mediaDevices
    .getUserMedia({video: true, audio: false}) // 这里我们只开启摄像头
    .then((stream) => { // 成功的执行
      video.srcObject = stream;
    })
    .catch((err) => { // 失败的执行
      console.error("An error occurred:" + err);
    })
},

当摄像头被成功启动之后,就会执行 canplay 方法

 canplay () {let { video, canvas} = this.$refs // 获取节点 
  if (!this.streaming) { // 是否是在运行中 如果是
    this.height = video.videoHeight / (video.videoWidth / this.width); // 初始化高度
    console.log(this.height)
    if (isNaN(this.height)) { // 这里初始化高度是为了和宽度进行一个响应 初始成一个 4:3 的比例
      this.height = this.width / (4 / 3);
    }
    video.setAttribute("width", this.width); // 这个时候开始设置节点的宽度和高度
    video.setAttribute("height", this.height);
    canvas.setAttribute("width", this.width);
    canvas.setAttribute("height", this.height);
    this.streaming = true;
  }
}

这个时候摄像头的基本也就已经操作完毕了 如果没有一场的时候就可以在页面上看到你摄像头所捕捉到的内容 这个时候我们需要把你想要捕捉的内容转成图片显示到页面上

takepicture () { // 点击方法 当我们点击 Take photo 按钮的时候 这个时候把这部分的信息转成图片
  let {video, canvas, photo} = this.$refs // 获取节点
  let context = canvas.getContext("2d")
  if (this.width && this.height) { // 把 video 的数据存入 canvas 以便我们进行其他操作 转成图片资源然后显示在页面上
    canvas.width = this.width;
    canvas.height = this.height;
    context.drawImage(video, 0, 0, this.width, this.height);
    let data = canvas.toDataURL("image/png");
    photo.setAttribute("src", data);
  } else { // 如果初始化的宽高不存在 这个时候说明有异常需要排除,同时清除掉画布上面的图片数据
    this.clearphoto();}
},
clearphoto () { // 清除画布图片数据 同时更新 img
  let {canvas, photo} = this.$refs
  var context = canvas.getContext("2d");
  context.fillStyle = "#AAA";
  context.fillRect(0, 0, canvas.width, canvas.height);

  var data = canvas.toDataURL("image/png");
  photo.setAttribute("src", data);
},

对摄像头捕捉的画面进行获取和解析

应用场景

完成以上步骤之后,没有问题。我们就可以唤醒连接的摄像头设备,捕捉你想要的画面存到你想要存的地方。当我们把捕捉到的画面存到 canvas 里面去之后,我们可以进行很多操作,可以对你捕捉的画面增加你想要的特效,这个时候 canvas 能够实现的功能都可以在你捕捉到画面上面实现,并且转成图片。
此时,摄像头所捕捉到的内容还是单向的,无法与别人通讯。如果你想实现一种类似于视频聊天的功能。这个时候就要改一下我们的代码,参考一下 webRTC 即时通讯协议。需要搭建一个第三方的信令服务器,来进行双向通讯。

人脸检测工具的使用

我现在借助的是的 face-api 这款工具 有捕捉人脸,检测面部特征。这个包可以把你想要检测的图片视频以及摄像头上面的内容都是支持捕捉人脸和检测面部特征。我这里主要实现的是摄像头捕捉人脸和检测面部特征。

官网:https://github.com/justadudew…

HTML 部分

<video id="video" autoplay muted></video>

JS 部分

const video = document.getElementById("video"); // 获取节点
Promise.all([ // 加载我想要使用的模型 同时对应的 josn 文件和 shard 文件要处在同一目录 不然读取的时候可能读取不到。当你读取不到的时候你可能会报 SyntaxError: Unexpected token < in JSON at position 0。这点略坑
  faceapi.nets.tinyFaceDetector.loadFromUri("/models"),
  faceapi.nets.faceLandmark68Net.loadFromUri("/models"),
  faceapi.nets.faceRecognitionNet.loadFromUri("/models"),
  faceapi.nets.faceExpressionNet.loadFromUri("/models")
]).then(startVideo); // 载入成功之后唤醒摄像头

function startVideo() {
  navigator.getUserMedia({ video: {} },
    stream => (video.srcObject = stream),
    err => console.error(err)
  );
}

video.addEventListener("play", () => { // 当摄像头成功启动之后 开始执行这部分的方法
  const canvas = faceapi.createCanvasFromMedia(video); // 创建一个画布
  document.body.append(canvas);
  const displaySize = {width: 640, height: 480}; // 这部分的大小是等同 video 的大小的
  faceapi.matchDimensions(canvas, displaySize); // 声明大小
  setInterval(async () => { // 我们需要持续传递摄像头的数据
    const detections = await faceapi
      .detectAllFaces(video, new faceapi.TinyFaceDetectorOptions())
      .withFaceLandmarks()
      .withFaceExpressions();
    const resizedDetections = faceapi.resizeResults(
      detections,
      displaySize
    );
    canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height);
    faceapi.draw.drawDetections(canvas, resizedDetections);
    faceapi.draw.drawFaceLandmarks(canvas, resizedDetections);
    faceapi.draw.drawFaceExpressions(canvas, resizedDetections);
  }, 100);
});

场景

正文完
 0