程序员眼中的小姐姐(canvas实现视频字符化)

56 阅读2分钟

在各位眼中肤白貌美大长腿的小姐姐,在我们程序员眼里都长啥样呢,没错就是长封面这样,为什么长这样,下面本小白来带各位看官去一探究竟

整体思路

首先实现这个功能需要准备两个canvas标签和一个video标签,video标签用来播放视频资源,监听video的某个事件,将视频按帧拆为图片,然后逐帧绘制在canvas1,利用canvas像素操作的API获取图片的像素数据,对像素数据进行处理,然后逐帧绘制在canvas2上。

具体实现

video需要做的事情

play事件中去递归调用requestAnimationFrame来逐帧渲染

  • player为video标签的dom对象,handleDataCanvas为绘制video图像的canvas,
//监听video的播放事件
    player.addEventListener(
    "play",
    () => {
      //根据视频的宽高比按比例缩小计算出canvas1的宽高
      handleDataCanvas.width = 50;
      handleDataCanvas.height =((50 / 1.5) * player.videoHeight) / player.videoWidth;
      const render = () => {
         // 自定义事件render
        const event = new CustomEvent("render");
        // 触发自定义事件
        player.dispatchEvent(event);
        if (!player.ended && !player.paused) {
        //判断video不是结束或者暂停时,递归调用requestAnimationFrame(为了提高性能)
          requestAnimationFrame(render);
        }
      };
      requestAnimationFrame(render);
    },
    false
  );

自定义事件中去调用核心方法draw

 
   player.addEventListener(
    "render",
    () => {
      draw();
    },
    false
  );

这里需要注意:canvas获取像素数据的API遵循同源策略,如果加载的视频资源涉及跨域问题,需要给videocrossOrigin属性设置为'';

核心代码

在这个方法中,canvas1获取像素数据,然后将获取到的像素数据进行处理,之后绘制到canvas2

处理像素

gray为像素灰度,根据公式R * 0.299 + B * 0.587 + G * 0.114;,将RGB带入公式计算得来
imgData为像素数据格式为:

{
    width:..,
    height:...,
    data:[],
}

其中width为水平方向像素点个数,height为垂直方向像素点个数,data则是一个一维数组,每三个元素表示一个像素的RGB值,是这些像素从左到右从上到下的一维描述

const pixelsTransform = (imgData) => {
  const pixelsData = imgData.data;
  const width = imgData.width;
  const height = imgData.height;
  const charData = [];
  // 使用两个循环遍历将一维数组转化为二维数组,这个二维数组中的每一个元素都是一行像素
  for (let row = 0; row < height; row++) {
    const rowData = []; // 行数据
    for (let col = 0; col < width; col++) {
      const R = pixelsData[(width * row + col) * 4 + 0];
      const G = pixelsData[(width * row + col) * 4 + 1];
      const B = pixelsData[(width * row + col) * 4 + 2];
      const gray = R * 0.299 + B * 0.587 + G * 0.114; // 计算灰度
      rowData.push({ gray, R, G, B }); //点数据装到行数据
    }
    charData.push(rowData);// 组装为二维数组
  }
  let map = ["#", "&", "$", "*", "!", ".", " "]; 
  //这里字符排列顺序也有讲究,按照复杂-->简单或者简单-->复杂的书序排列,对应着不同灰度
  return charData.map((row) => {
    return row.map(({ gray, R, G, B }) => {
      let txt = map[Math.floor((gray / 256) * map.length)];// 根据灰度值按比例计算出字符下标
      //返回要渲染的字符以及该字符的颜色值
      return { txt, R, G, B };
    });
  });
};

draw方法的实现

displayCtxcanvas2的2d上下文对象,通过canvasdom对象的getContext("2d");方法获取,handleDataCtxcanvas1的2d上下文对象,获取方法同上


const draw = (colorful=false,charColor='#000') => {
// 每次绘制清空画布
  displayCtx.clearRect(
    0,
    0,
    displayCanvas.width,
    displayCanvas.height
  );
 // 将video绘制到canvas1
  handleDataCtx.drawImage(
    player.value,
    0,
    0,
    handleDataCanvas.width,
    handleDataCanvas.height
  );
  //通过canvas1获取图像像素数据
  const imgData = handleDataCtx.getImageData(
    0,
    0,
    handleDataCanvas.width,
    handleDataCanvas.height
  );
  //调用处理像素的函数获取到字符数据
  const charData = pixelsTransform(imgData);
  //遍历字符数据,绘制到canvas2
  charData.forEach((row, rowIndex) => {
    const fontSize = Math.floor(displayCanvas.value.width / row.length);
    // 这里使用画布宽度/每行字符数得到字体大小
    const offsetY = Math.floor(
      (props.size.height - charData.length * fontSize) / 2
    );
    /**
     * charData.length * fontSize为在画布中绘制的图像高度
     * Y轴方向偏移量为 (画布高度-图像高度)/2
     */
    displayCtx.font = `${fontSize}px arial`;
    // 使用等宽字体 确保上面计算结果在X方向铺满画布,Y方向居中
    row.forEach((col, colIndex) => {
      const { txt, R, G, B } = col;
      // 是否绘制彩色图像
      if (colorful) {
        displayCtx.fillStyle = `rgb(${R},${G},${B})`;
      } else {
      //如果不是绘制彩色则看第二参数指定的字符颜色
        displayCtx.fillStyle = charColor;
      }
      displayCtx.fillText(
        txt,
        fontSize * colIndex,
        fontSize * rowIndex + offsetY
      );
    });
  });
};

现成的组件

这是个基于vue写的视频字符化的组件

在线示例