自学内容网 自学内容网

如何在前端给视频进行去除绿幕并替换背景?-----Vue3!!

最近在做这个这项目奇店桶装水小程序V1.3.9安装包+骑手端V2.0.1+小程序前端        

       最近,我在进行前端开发时,遇到了一个难题“如何给前端的视频进行去除绿幕并替换背景”。这是一个“数字人项目”所需,我一直在冥思苦想。终于有了一个解决方法——使用Canvas来处理。

        这是真材实料的文章——让你的Canvas的技术更上一层楼!!!


效果图 


实现思路

1. 准备工作 视频和画布元素:在HTML模板中定义了一个<video>标签用于播放视频,以及一个<canvas>标签用来绘制处理后的视频帧。 初始化:在组件挂载(mounted)时,获取视频和画布元素,并初始化绘图上下文。

<template>
  <div class="videoBgRemove">
    <!-- 视频元素 -->
    <video ref="video" loop autoplay muted style="width: 240px;">
      <source src="/8_1736396574.mp4" type="video/mp4">
      Your browser does not support the video tag.
    </video>
    <!-- 画布元素 -->
    <canvas ref="canvas" width="200" height="450"></canvas>
  </div>
</template>

<script>
export default {
  data() {
    return {
      featherStrength: 0.4, // 羽化强度控制
    };
  },
  mounted() {
    // 初始化视频和画布引用
    this.video = this.$refs.video;
    this.canvas = this.$refs.canvas;
    this.ctx = this.canvas.getContext('2d');
    this.canvas_tmp = document.createElement('canvas');
    this.canvas_tmp.width = this.canvas.width;
    this.canvas_tmp.height = this.canvas.height;
    this.ctx_tmp = this.canvas_tmp.getContext('2d');

    // 初始化其他变量
    this.init();
  },
  methods: {
    init() {
      // 当视频开始播放时,调用computeFrame进行逐帧处理
      this.video.addEventListener('play', this.computeFrame);
    }
  }
};
</script>

2. 视频帧处理逻辑 逐帧处理:当视频开始播放时,computeFrame函数会不断被调用,每次调用都会处理一帧视频数据。 临时画布:为了不影响原始视频的播放,所有图像处理都在一个临时创建的画布(canvas_tmp)上进行。 图像数据获取:从临时画布上获取当前帧的图像数据(像素信息)以进行处理。

methods: {
  computeFrame() {
    if (!this.video || this.video.paused || this.video.ended) return;

    // 绘制当前帧到临时画布上
    this.ctx_tmp.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);

    // 获取当前帧的图像数据
    let frame = this.ctx_tmp.getImageData(0, 0, this.canvas.width, this.canvas.height);
    
    // 后续处理...
  }
}

3. 背景移除 颜色检测:假设背景为特定的颜色(例如绿色),对于每个像素点,如果其RGB值符合预设的背景颜色范围,则将其alpha通道设置为0,即变为透明。

methods: {
  computeFrame() {
    // ... (前面的代码)

    const pointLens = frame.data.length / 4;

    // 遍历每一个像素点
    for (let i = 0; i < pointLens; i++) {
      let r = frame.data[i * 4];
      let g = frame.data[i * 4 + 1];
      let b = frame.data[i * 4 + 2];
      
      // 假设背景是绿色,将符合条件的像素设置为透明
      if (r < 100 && g > 120 && b < 200) { 
        frame.data[i * 4 + 3] = 0; // 设置alpha通道为0,使背景透明
      }
    }

    // 后续处理...
  }
}

4. 羽化效果 边缘检测与平均:对于非透明的像素,计算它周围的像素,取周围像素颜色的平均值作为新颜色,并根据周围的透明度调整当前像素的透明度,以此来实现羽化效果。 强度控制:通过featherStrength参数可以控制羽化的程度,从而让边缘过渡更加自然。

methods: {
  computeFrame() {
    // ... (前面的代码)

    // 创建一个临时的数据副本,避免修改原始数据
    const tempData = [...frame.data];

    // 对非透明像素应用羽化效果
    for (let i = 0; i < pointLens; i++) {
      if (frame.data[i * 4 + 3] === 0) continue; // 忽略已经透明的像素

      // 计算当前像素的位置
      let [row, col] = this.numToPoint(i + 1, frame.width);

      // 获取周围的像素点
      let aroundPoints = this.getAroundPoint([row, col], frame.width, frame.height, 3);

      // 计算周围非透明像素的颜色平均值
      let opNum = 0;
      let rSum = 0;
      let gSum = 0;
      let bSum = 0;
      aroundPoints.forEach(([pRow, pCol]) => {
        let index = this.pointToNum([pRow, pCol], frame.width);
        rSum += tempData[(index - 1) * 4];
        gSum += tempData[(index - 1) * 4 + 1];
        bSum += tempData[(index - 1) * 4 + 2];
        if (tempData[(index - 1) * 4 + 3] !== 255) opNum++;
      });

      // 计算新的alpha值
      let alpha = (255 / aroundPoints.length) * (aroundPoints.length - opNum);

      // 根据羽化强度调整alpha
      if (alpha !== 255) {
        frame.data[i * 4] = parseInt(rSum / aroundPoints.length);
        frame.data[i * 4 + 1] = parseInt(gSum / aroundPoints.length);
        frame.data[i * 4 + 2] = parseInt(bSum / aroundPoints.length);
        frame.data[i * 4 + 3] = parseInt(alpha * this.featherStrength);
      }
    }

    // 将处理后的图像数据绘制到实际显示的画布上
    this.ctx.putImageData(frame, 0, 0);

    // 持续循环
    requestAnimationFrame(this.computeFrame);
  },

  numToPoint(num, width) {
    let col = num % width;
    let row = Math.floor(num / width);
    return [row + 1, col === 0 ? width : col];
  },

  pointToNum(point, width) {
    let [row, col] = point;
    return (row - 1) * width + col;
  },

  getAroundPoint(point, width, height, area) {
    let [row, col] = point;
    let allAround = [];
    for (let i = -Math.floor(area / 2); i <= Math.floor(area / 2); i++) {
      for (let j = -Math.floor(area / 2); j <= Math.floor(area / 2); j++) {
        if (i === 0 && j === 0) continue; // 跳过中心点
        let pRow = row + i;
        let pCol = col + j;
        if (pRow > 0 && pCol > 0 && pRow <= height && pCol <= width) {
          allAround.push([pRow, pCol]);
        }
      }
    }
    return allAround;
  }
}

5. 显示处理结果 更新画布:将处理后的图像数据应用到实际显示的画布(canvas)上,这样用户就能看到带有透明背景和羽化效果的视频了。

// 将处理后的图像数据绘制到实际显示的画布上
this.ctx.putImageData(frame, 0, 0);

6. 持续循环 递归调用:computeFrame函数会在每一帧处理完毕后立即再次调用自己,形成一个持续的循环,直到视频停止播放。代码加在这里面。

methods: {
  computeFrame() {
    // ... (前面的代码)

    // 持续循环
    requestAnimationFrame(this.computeFrame);
  }
}

完整的demo

1.App.vue

<template>
  <div id="app">
    <h1>背景人像处理</h1>
    <VideoRemoval />
  </div>
</template>

<script>
import VideoRemoval from './components/VideoRemoval.vue';

export default {
  name: 'App',
  components: {
    VideoRemoval
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;

  background-image: url("../src/assets/web_bg.jpg"); /* 使用正确的路径 */
  background-size: cover; /* 背景图片覆盖整个容器 */
  background-position: center center; /* 背景图片居中显示 */
  background-repeat: no-repeat; /* 防止背景图片重复 */
  background-attachment: fixed; /* 背景固定在视口 */
}
</style>

2.VideoRemoval.vue

<template>
  <div class="videoBgRemove">
    <video id="video"
           src="/8_1736396574.mp4"
           loop
           autoplay
           muted
           ref="video"
           style="width: 240px;"></video>
    <canvas id="output-canvas"
            width="200"
            height="450"
            willReadFrequently="true"
            ref="canvas"></canvas>
  </div>
</template>

<script>
export default {
  data () {
    return {
      video: null,
      canvas: null,
      ctx: null,
      canvas_tmp: null,
      ctx_tmp: null,
      featherStrength: 0.4, // 羽化强度控制
    };
  },
  methods: {
    init () {
      this.ctx = this.canvas.getContext('2d');
      this.canvas_tmp = document.createElement('canvas');
      this.canvas_tmp.setAttribute('width', 200);
      this.canvas_tmp.setAttribute('height', 450);
      this.ctx_tmp = this.canvas_tmp.getContext('2d');
      this.video.addEventListener('play', this.computeFrame);
    },

    numToPoint (num, width) {
      let col = num % width;
      let row = Math.floor(num / width);
      row = col === 0 ? row : row + 1;
      col = col === 0 ? width : col;
      return [row, col];
    },

    pointToNum (point, width) {
      let [row, col] = point;
      return (row - 1) * width + col;
    },

    getAroundPoint (point, width, height, area) {
      let [row, col] = point;
      let allAround = [];
      if (row > height || col > width || row < 0 || col < 0) return allAround;
      for (let i = 0; i < area; i++) {
        let pRow = row - 1 + i;
        for (let j = 0; j < area; j++) {
          let pCol = col - 1 + j;
          if (i === area % 2 && j === area % 2) continue;
          allAround.push([pRow, pCol]);
        }
      }
      return allAround.filter(([iRow, iCol]) => {
        return iRow > 0 && iCol > 0 && iRow <= height && iCol <= width;
      });
    },

    computeFrame () {
      if (this.video) {
        if (this.video.paused || this.video.ended) return;
      }
      this.ctx_tmp.drawImage(this.video, 0, 0, this.video.clientWidth, this.video.clientHeight);

      let frame = this.ctx_tmp.getImageData(0, 0, this.video.clientWidth, this.video.clientHeight);

      const height = frame.height;
      const width = frame.width;
      const pointLens = frame.data.length / 4;

      // 背景透明化(假设背景为特定颜色,这里选择绿色)
      for (let i = 0; i < pointLens; i++) {
        let r = frame.data[i * 4];
        let g = frame.data[i * 4 + 1];
        let b = frame.data[i * 4 + 2];
        if (r < 100 && g > 120 && b < 200) {
          frame.data[i * 4 + 3] = 0;
        }
      }

      const tempData = [...frame.data];
      for (let i = 0; i < pointLens; i++) {
        if (frame.data[i * 4 + 3] === 0) continue;
        const currentPoint = this.numToPoint(i + 1, width);
        const arroundPoint = this.getAroundPoint(currentPoint, width, height, 3);

        let opNum = 0;
        let rSum = 0;
        let gSum = 0;
        let bSum = 0;
        arroundPoint.forEach((position) => {
          const index = this.pointToNum(position, width);
          rSum += tempData[(index - 1) * 4];
          gSum += tempData[(index - 1) * 4 + 1];
          bSum += tempData[(index - 1) * 4 + 2];
          if (tempData[(index - 1) * 4 + 3] !== 255) opNum++;
        });

        let alpha = (255 / arroundPoint.length) * (arroundPoint.length - opNum);

        // 调整羽化效果
        if (alpha !== 255) {
          frame.data[i * 4] = parseInt(rSum / arroundPoint.length);
          frame.data[i * 4 + 1] = parseInt(gSum / arroundPoint.length);
          frame.data[i * 4 + 2] = parseInt(bSum / arroundPoint.length);

          // 根据羽化强度调整 alpha
          frame.data[i * 4 + 3] = parseInt(alpha * this.featherStrength);
        }
      }

      this.ctx.putImageData(frame, 0, 0);
      setTimeout(this.computeFrame, 0);
    }
  },

  mounted () {
    this.video = this.$refs.video;
    this.canvas = this.$refs.canvas;
    this.init();
  }
};
</script>


完整项目demo在前端给视频去除绿幕并替换背景: 最近,我在进行前端开发时,遇到了一个难题“如何给前端的视频进行去除绿幕并替换背景”。这是一个“数字人项目”所需,我一直在冥思苦想。终于有了一个解决方法——使用Canvas来处理。         这是真材实料的文章——让你的Canvas的技术更上一层楼!!!


原文地址:https://blog.csdn.net/2301_81807150/article/details/145165206

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!