原生Js Canvas去除視頻綠幕背景的方法實現(xiàn)
注: 這里的去除視頻背景并不是對視頻文件進(jìn)行操作去除背景
如果需要對視頻扣除背景并導(dǎo)出可以使用 ffmpeg 等庫,這里僅作播放用所以采用這種方法
由于uniapp中的canvas經(jīng)過封裝,且 uniapp 的
drawImage無法繪制視頻幀畫面,因此uniapp中不適用
實現(xiàn)過程是將視頻使用canvas逐幀截下來對截取的圖片進(jìn)行處理,然后在canvas中顯示處理好的圖片
最后通過定時器高速處理替換,形成視頻播放的效果,效果如下圖?

邊緣仍然會有些綠幕的像素,可以通過其他的處理進(jìn)行優(yōu)化
原理
首先使用canvas的 drawImage 方法將video的當(dāng)前幀畫面繪制到canvas中
然后再通過 getImageData 方法獲取當(dāng)前canvas的所有像素的 rgba 值組成的數(shù)組
獲取到的值為 [r,g,b,a,r,g,b,a,...] ,每一組 rgba 的值就是一個像素,所以獲取到的數(shù)組長度是canvas的像素的數(shù)量 * 4
通過判斷每一組 rgb 的值是否為綠幕像素,然后設(shè)置其透明通道的 alpha 的值為0實現(xiàn)效果
代碼
因為canvas會受到跨域的影響導(dǎo)致畫布被污染,因此首先需要將測試視頻下載到本地
如果直接本地打開html的話同樣會因為本地路徑報跨域錯誤,需要將html,js,測試視頻放在文件夾中部署一個本地服務(wù)器
可以使用 http-server
npm i http-server -g # 切換到存放html,js,測試視頻的文件夾 運(yùn)行命令即可部署本地服務(wù)器 http-server
或者
vsCode的 Live server 插件均可
<!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>// 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');
// 創(chuàng)建的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比例不正確可能會出現(xiàn)顯示形變, 調(diào)整除的值進(jìn)行比例調(diào)整
ctx_tmp.drawImage(video, 0, 0, video.clientWidth / 1, video.clientHeight / 1);
// 獲取到繪制的canvas的所有像素rgba值組成的數(shù)組
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值在這個范圍內(nèi)則是綠幕背景,設(shè)置alpha值為0
// 同理不同顏色的背景調(diào)整rgb的判斷范圍即可
if (r < 100 && g > 120 && b < 200) {
frame.data[i * 4 + 3] = 0;
}
}
// 重新繪制到canvas中顯示
ctx.putImageData(frame, 0, 0);
// 遞歸調(diào)用
setTimeout(computeFrame, 0);
}
document.addEventListener("DOMContentLoaded", () => {
init();
});使用本地服務(wù)器訪問html即可看到效果,可以看到邊緣仍有綠色像素閃爍
一般情況這種就可以了,使用算法進(jìn)行處理的話效果會更好,但相應(yīng)的資源的消耗也會提升,造成幀率下降
下面展示通過一些算法進(jìn)行羽化和顏色過渡
羽化
// 返回canvas中第num個像素點所在的坐標(biāo) 12 -> [1, 12]
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];
}
// 返回canvas中所在坐標(biāo)的num(index + 1)值 [1, 12] -> 12
function pointToNum (point, width) {
let [row, col] = point;
return (row - 1) * width + col
}
// 獲取輸入的坐標(biāo)周圍1像素內(nèi)的所有像素的坐標(biāo)組成的數(shù)組 [1, 1] -> [[1, 2], [2, 1], [2, 2]]
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);
})
}通過上面的函數(shù)獲取到一個選定的不透明的像素周圍的像素后,判斷周圍的像素的alpha值
如果周圍的像素有存在透明的像素,則重新計算選定像素的alpha值
顏色過渡
計算修改alpha值連帶計算周圍像素中rgb的各項平均值給選定像素
最終處理結(jié)果如下

代碼
// 新增羽化和顏色過渡
// 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');
// 創(chuàng)建的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();
});到此這篇關(guān)于原生Js Canvas去除視頻綠幕背景的方法實現(xiàn)的文章就介紹到這了,更多相關(guān)Js Canvas去除視頻綠幕背景內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
仿iPhone通訊錄制作小程序自定義選擇組件的實現(xiàn)
這篇文章主要介紹了仿iPhone通訊錄制作小程序自定義選擇組件的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05
javascript 在網(wǎng)頁中的運(yùn)用(asp.net)
javascript在網(wǎng)頁中的運(yùn)用實現(xiàn),需要的朋友可以參考下。2009-11-11
javascript:;與javascript:void(0)使用介紹
有時候我們在編寫js過程中,需要觸發(fā)事件而不需要返回值,那么就可能需要這樣的寫法2013-06-06
layui 表格操作列按鈕動態(tài)顯示的實現(xiàn)方法
今天小編就為大家分享一篇layui 表格操作列按鈕動態(tài)顯示的實現(xiàn)方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-09-09

