对实时捕获视频画面取色

全篇共 4604 字。按500字/分钟阅读速度,阅读完预计需要 9.2 分钟。

基于MediaDevice这一Web API捕获摄像头拍摄的实时画面,将视频画面即时渲染至Canvas画布上,基于Airglass.js处理与取色相关的交互操作。

渲染视频捕获画面

调用MediaDevices API的getUserMedia()方法,用户代理会自动处理用户是否授权开发者访问音视频输入流,开发者只需编写授权成功和授权失败后的处理函数。当授权成功后,开发者就能在处理函数中拿到stream输入流。将输入流赋值给HTML5中的视频标签 <video></video>对应的DOM元素的srcObject属性,然后再调用它的 play() 方法即可实时播放从用户摄像头捕获的画面。

let videoEl = document.querySelector('#video');

navigator.mediaDevices.getUserMedia({
  audio: true,
  video: true
})
.then(stream => {
  videoEl.srcObject = stream;
  videoEl.play();
});

Canvas API的 drawImage() 方法的第一个参数,不单单能传递一个Image实例,还能传递一个Video实例。开发者想要操作实时画面中的像素,就需要想办法将视频画面实时地渲染到Canvas画布中。捕获到的视频画面的尺寸受各式各样的硬件设备配置参数的影响,Canvas画布的尺寸又需要适配用户的显示设备,所以,将视频画面渲染到Canvas上就需要一套算法适配各式各样的视频画面尺寸。

(function render(){
  requestAnimationFrame(render);
  if (!videoEl.videoWidth || !videoEl.videoHeight) return;
  let x = 0;
  let y = 0;
  let width = videoEl.videoWidth;
  let height = videoEl.videoHeight;
  let videoRatio = videoEl.videoWidth / videoEl.videoHeight;
  let agRatio = ag.width / ag.height;
  if (agRatio > videoRatio) {
    width = ag.width;
    height = parseInt(width / videoRatio);
    if (height > ag.height) {
      y = parseInt((ag.height - height) / 2);
    }
  } else {
    height = ag.height;
    width = parseInt(videoRatio * height);
    if (width > ag.width) {
      x = parseInt((ag.width - width) / 2);
    }
  }
  realtimeCtx.drawImage(videoEl, 0, 0, videoEl.videoWidth, videoEl.videoHeight, x, y, width, height);
})();

取色器组件

得益于Airglass.js对发生在Canvas画布上的丰富的事件类型的高效处理和友好反馈,比如TouchStart、TouchMove、TouchEnd等事件,开发者可以轻松实现拖拽取色器的能力。Canvas API提供的 getImageData() 方法,让开发者能获取单个像素或一块区域所有像素的颜色信息。

开发者有能力继承 airglass.Renderable 类实现自定义的可渲染组件,比如自定义的取色器组件。由于取色器组件需要响应用户触发的的TouchStart和TouchMove事件,所以取色器组件必须包含 path 属性,用以存储当前取色器发生交互的路径区域。

const ColorSelector = airglass.extend(airglass.Renderable, {
  _constructor(params) {
    this.x = params && params.x || 0;
    this.y = params && params.y || 0;
    this.size = params && params.size || 16;
    this.sourceCtx = params.sourceCtx;
    this.path = new Path2D;
    this.r = 0;
    this.g = 0;
    this.b = 0;
  },
  draw(ctx) {
    let imgData = this.sourceCtx.getImageData(this.x, this.y, 1, 1);
    this.r = imgData.data[0];
    this.g = imgData.data[1];
    this.b = imgData.data[2];
    this.path = new Path2D;
    this.path.arc(this.x, this.y, this.size, 0, Math.PI * 2, true);
    ctx.save();
    ctx.fillStyle = `rgb(${this.r}, ${this.g}, ${this.b})`;
    ctx.strokeStyle = '#fff';
    ctx.lineWidth = 3;
    ctx.fill(this.path);
    ctx.stroke(this.path);
    ctx.restore();
  }
});

常见Canvas图像处理算法

首先提取整个Canvas画布的像素数据,之后循环遍历每一个像素的颜色值,其中每4个数字为一组,这4个数字分表代表R(红色)、G(绿色)、B(蓝色)和A(不透明度),每一组数字表示一个像素的颜色,这4个数字的取值范围在0~255之间。修改每个像素的颜色值时,并不是凭空设置,而是在每个像素的原有颜色值的基础上,经过一些计算,得到每个像素最终的颜色值。Canvas API的 putImageData() 方法让开发者能将修改完颜色后的像素渲染到Canvas画布中。

  1. 负片效果,又称底片效果、反相效果、反色效果。因为R、G、B三个颜色通道的数值取值范围在0~255之间,因此,用每个像素的3个颜色通道的最大值255减去该通过的原始值,就能得到每个像素反色后应该设置的3个颜色值。比如某个像素的红色通道值为5,说明这个像素的红色很少,用255减去5得到250,最终这个像素的红色通道值变成了250,那么这个像素的红色看起来就很多。所以负片效果的本质就是将每个像素原本很浓的颜色变淡,将原本很淡的颜色变浓,从宏观来看,整幅图像就有了底片的效果。

const imgData = realtimeCtx.getImageData(0, 0, ag.width, ag.height);
for (let i = 0; i < imgData.data.length; i += 4) {
  let r = imgData.data[i];
  let g = imgData.data[i + 1];
  let b = imgData.data[i + 2];
  let a = imgData.data[i + 3];
  imgData.data[i] = 255 - r;
  imgData.data[i + 1] = 255 - g;
  imgData.data[i + 2] = 255 - b;
  imgData.data[i + 3] = 255;
}
  1. 灰度效果。灰度并不是将整幅图像的所有像素的RGB通道都设置为同一个0~255之间的数值,这会让整幅图像变成灰色。真正的灰度效果是让每一个像素的RGB通道值“齐头并进”——计算并得出同一个值,而这个值在各个像素之间却不尽相同。

const imgData = realtimeCtx.getImageData(0, 0, ag.width, ag.height);
for (let i = 0; i < imgData.data.length; i += 4) {
  let red = imgData.data[i];
  let green = imgData.data[i + 1];
  let blue = imgData.data[i + 2];
  let gray = 0.3 * red + 0.59 * green + 0.11 * blue;
  imgData.data[i] = gray;
  imgData.data[i + 1] = gray;
  imgData.data[i + 2] = gray;
}
  1. 单色效果。制造单色效果,只需把R、G、B这3个颜色通道中的任意2个关闭即可。以红色通道为例,比如将红色通道设置为255,就是将红色通道完全打开,就像一个很通畅的管道,纯红色全量通过,反映到每一个像素上,图像偏红。若给它设置为0,就是将红色通道完全关闭,就像一个完全堵塞住的管道,红色全部无法通过,反映到每一个像素上,图像一点红色都没有。若设置为0~255之间的值,值越接近255,红色越纯越亮。未必一定要将3个通道中的某两个通过关闭来营造纯红、纯绿、纯蓝这三种单色效果,如果想制造橙色、青色、紫色的单色效果,就需要RGB三原色共同参与,恰当调和。

const imgData = realtimeCtx.getImageData(0, 0, ag.width, ag.height);
for (let i = 0; i < imgData.data.length; i += 4) {
  let red = imgData.data[i];
  let green = imgData.data[i + 1];
  let blue = imgData.data[i + 2];
  imgData.data[i] = red;
  imgData.data[i + 1] = 0;
  imgData.data[i + 2] = 0;
}
  1. 非黑即白效果。

const imgData = realtimeCtx.getImageData(0, 0, ag.width, ag.height);
for (let i = 0; i < imgData.data.length; i += 4) {
  let red = imgData.data[i];
  let green = imgData.data[i + 1];
  let blue = imgData.data[i + 2];
  let gray = 0.6 * red + 0.59 * green + 0.11 * blue;
  let black;
  gray > 100 ? black = 255 : black = 0;
  imgData.data[i] = black;
  imgData.data[i + 1] = black;
  imgData.data[i + 2] = black;
}
原创作者 » 陈帅华
版权声明 » 自由转载-保持署名-非商用-非衍生
发布日期 » 2020年4月15日 周三
更新日期 » 2020年6月11日 周四
上一篇 » 甘特图在线编辑工具
下一篇 » [译] Deno 1.0发布
:)记录此刻想法
请选择登录方式,开始记录你的想法。
授权微博登录
授权Github登录