原生Js Canvas去除视频绿幕背景

Js去除视频背景

注: 这里的去除视频背景并不是对视频文件进行操作去除背景

如果需要对视频扣除背景并导出可以使用ffmpeg等库,这里仅作播放用所以采用这种方法

由于uniapp中的canvas经过封装,且 uniapp 的 drawImage 无法绘制视频帧画面,因此uniapp中不适用

实现过程是将视频使用canvas逐帧截下来对截取的图片进行处理,然后在canvas中显示处理好的图片

最后通过定时器高速处理替换,形成视频播放的效果,效果如下图⬇

边缘仍然会有些绿幕的像素,可以通过其他的处理进行优化

原理

首先使用canvas的 drawImage 方法将video的当前帧画面绘制到canvas中

然后再通过 getImageData 方法获取当前canvas的所有像素的rgba值组成的数组

获取到的值为[r,g,b,a,r,g,b,a,...],每一组rgba的值就是一个像素,所以获取到的数组长度是canvas的像素的数量 * 4

通过判断每一组rgb的值是否为绿幕像素,然后设置其透明通道的alpha的值为0实现效果

代码

因为canvas会受到跨域的影响导致画布被污染,因此首先需要将测试视频下载到本地

如果直接本地打开html的话同样会因为本地路径报跨域错误,需要将html,js,测试视频放在文件夹中部署一个本地服务器

可以使用http-server

bash 复制代码
npm i http-server -g

# 切换到存放html,js,测试视频的文件夹 运行命令即可部署本地服务器

http-server

或者

vsCode的Live server插件均可

测试视频 地址

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <style>
        video{
            width: 480px;
            height: 270px;
        }
    </style>
  </head>

  <body>
    <video id="video"  src="./63e1dd7ddd2b0.mp4"  loop autoplay muted></video>
    <canvas id="output-canvas" width="480" height="270" willReadFrequently="true"></canvas>
    <script type="text/javascript" src="processor2.js"></script>
  </body>
</html>
js 复制代码
// processor2.js

let video, canvas, ctx, canvas_tmp, ctx_tmp;

function init () {
    video = document.getElementById('video');
    canvas = document.getElementById('output-canvas');
    ctx = canvas.getContext('2d');
    
	// 创建的canvas宽高最好与显示图片的canvas、video宽高一致
    canvas_tmp = document.createElement('canvas');
    canvas_tmp.setAttribute('width', 480);
    canvas_tmp.setAttribute('height', 270);
    ctx_tmp = canvas_tmp.getContext('2d');

    video.addEventListener('play', computeFrame);
}

function computeFrame () {
    if (video) {
        if (video.paused || video.ended) return;
    }
    // 如果视频比例和canvas比例不正确可能会出现显示形变, 调整除的值进行比例调整
    ctx_tmp.drawImage(video, 0, 0, video.clientWidth / 1, video.clientHeight / 1);

	// 获取到绘制的canvas的所有像素rgba值组成的数组
    let frame = ctx_tmp.getImageData(0, 0, video.clientWidth, video.clientHeight);

	// 共有多少像素点
    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];
        
        // 判断如果rgb值在这个范围内则是绿幕背景,设置alpha值为0 
        // 同理不同颜色的背景调整rgb的判断范围即可
        if (r < 100 && g > 120 && b < 200) {
            frame.data[i * 4 + 3] = 0;
        }
    }
    
    // 重新绘制到canvas中显示
    ctx.putImageData(frame, 0, 0);
    // 递归调用
    setTimeout(computeFrame, 0);
}


document.addEventListener("DOMContentLoaded", () => {
    init();
});

使用本地服务器访问html即可看到效果,可以看到边缘仍有绿色像素闪烁

一般情况这种就可以了,使用算法进行处理的话效果会更好,但相应的资源的消耗也会提升,造成帧率下降

下面展示通过一些算法进行羽化和颜色过渡

优化

羽化

通过上述 rgb 值的筛选后仍有一些绿幕像素由于 rgb 值与人物颜色相近而无法处理,

扩大 rgb 值的筛选范围又导致人物像素出现镂空,因此我们需要对边缘的像素进行处理。

  1. 获取处理像素的 3x3 范围内的像素

    假设 x 为我们需要处理的像素值, 获取周围的所有像素 -> 1, 2, 3, 4, 6, 7, 8, 9
    [
    [1, 2, 3],
    [4, x, 6],
    [7, 8, 9],
    ]

  2. 计算周围所有像素中透明通道为 0 的个数

    假设透明通道为 0 的是 1, 2, 3
    [
    [0 , 0 , 0 ],
    [255, x , 255],
    [255, 255, 255],
    ]

  3. 重新计算处理像素的 alpha

由于 x 周围的透明像素有3个,那么 xalpha 值为 (255 / 8) * (8 - 3)

相当于 x 周围有几个像素就把 255 分为几份,周围每有一个像素的 alpha 为 0, 就减一份。

计算完之后把结果赋给 xalpha 值。

注意:

由于遍历时 前一个像素的修改 会影响 后一个像素获取周围的值。

对 x 的修改会影响 y 的计算
[
    [1, 2, 3 , 4 ],
    [5, x, y , 8 ],
    [9, 1, 11, 12],
]

因此需要将第一次 rgb 筛选后的数据做一次深拷贝,获取的值基于拷贝后的值。

颜色过渡

在计算 alpha 值的时候将周围像素的 rgb 各个通道的值求和计算出平均值,

修改处理像素的 alpha 值时连带 rgb 值一起修改。

最终处理结果如下

代码(优化)

js 复制代码
// 新增羽化和颜色过渡

// processor2.js
let video, canvas, ctx, canvas_tmp, ctx_tmp;

function init () {
    video = document.getElementById('video');
    canvas = document.getElementById('output-canvas');
    ctx = canvas.getContext('2d');
    
	// 创建的canvas宽高最好与显示图片的canvas、video宽高一致
    canvas_tmp = document.createElement('canvas');
    canvas_tmp.setAttribute('width', 480);
    canvas_tmp.setAttribute('height', 270);
    ctx_tmp = canvas_tmp.getContext('2d');

    video.addEventListener('play', computeFrame);
}


function 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];
}

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

function 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);
    })
}

function computeFrame () {
    if (video) {
        if (video.paused || video.ended) return;
    }
    ctx_tmp.drawImage(video, 0, 0, video.clientWidth, video.clientHeight);
    let frame = ctx_tmp.getImageData(0, 0, video.clientWidth, video.clientHeight);

    //----- emergence ----------
    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 < 150 && g > 200 && b < 150) {
            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 = numToPoint(i + 1, width);
        const arroundPoint = getAroundPoint(currentPoint, width, height, 3);
        let opNum = 0;
        let rSum = 0;
        let gSum = 0;
        let bSum = 0;
        arroundPoint.forEach((position) => {
            const index = pointToNum(position, width);
            rSum = rSum + tempData[(index - 1) * 4];
            gSum = gSum + tempData[(index - 1) * 4 + 1];
            bSum = 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) {
            // debugger
            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);
            frame.data[i * 4 + 3] = parseInt(alpha);
        }
    }

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


document.addEventListener("DOMContentLoaded", () => {
    init();
});
相关推荐
阿伟来咯~24 分钟前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端29 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱32 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai41 分钟前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨42 分钟前
在JS中, 0 == [0] 吗
开发语言·javascript
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
aPurpleBerry3 小时前
JS常用数组方法 reduce filter find forEach
javascript
ZL不懂前端4 小时前
Content Security Policy (CSP)
前端·javascript·面试