使用 Canvas 的视频作为视频的遮罩

Using Video as Mask of Video with Canvas

我一直在尝试创建一个从一个视频到另一个视频的动态遮罩。这两个视频都没有 alpha 通道(即便如此,现在浏览器对它的支持也很低),所以我试图用 canvas 解决这个问题。我设法在非常有限的周期内完成了它,但它仍然无法在我的输出中以合适的帧速率完成此操作(它达到大约 20 fps,并且至少应该达到 25/30 才能显得平滑)。它实际上是 运行 的单数形式,周围有其他元素,所以我需要提高它的效率。

由于保密原因,我不能真正分享实际的视频,但其中一个包含黑色和白色的 alpha 掩码 (mask.mp4)(以及介于两者之间的灰色阴影),而另一个视频内容可以是任何需要屏蔽的内容 (video.mp4).

我要改进的代码实际上是这样的(假设所有视频和canvas元素都是1920x1080):

const video = document.getElementById( 'video' ); // `<video src="video.mp4"></video>
const mask = document.getElementById( 'mask' ); // `<video src="mask.mp4"></video>
const buffer = document.getElementById( 'buffer' ); // <canvas hidden></canvas>
const bufferCtx = buffer.getContext( '2d' );
const output = document.getElementById( 'output' ); // <canvas></canvas>
const outputCtx = output.getContext( '2d' );

outputCtx.globalCompositeOperation = 'source-in';

function maskVideo(){
    
    // Draw the mask
    bufferCtx.drawImage( mask, 0, 0 );
    
    // Get image data
    const data = bufferCtx.getImageData( 0, 0, w, h );
     
    // Assign the grayscale value to the alpha in the image data
    for( let i = 0; i < data.data.length; i += 4 ){
    
        data.data[i+3] = data.data[i];
    
    }
    
    // Put in the data
    outputCtx.putImageData( data, 0, 0 );
    // Draw the masked video into the mask (see the globalCompositeOperation above)
    outputCtx.drawImage( video, 0, 0, w, h );
    
    window.requestAnimationFrame( maskVideo );

}

window.requestAnimationFrame( maskVideo );

所以我的问题是:当两个视频都是 运行 时,是否有更有效的方法来屏蔽另一个视频 中的黑色像素?我目前并不担心并发性(两个流之间的差异小于一毫秒,远小于视频本身的实际帧)。 目标是提高帧率。

这是更广泛的代码片段,不幸的是,由于明显的 CORS 和视频托管原因,它无法处理堆栈溢出:

const video = document.getElementById( 'video' );
const mask = document.getElementById( 'mask' );
const buffer = document.getElementById( 'buffer' );
const bufferCtx = buffer.getContext( '2d' );
const output = document.getElementById( 'output' );
const outputCtx = output.getContext( '2d' );
const fpsOutput = document.getElementById( 'fps' );
const w = 1920;
const h = 1080;

let currentError;
let FPSCount = 0;

output.width =
buffer.width = w;
output.height =
buffer.height = h;

outputCtx.globalCompositeOperation = 'source-in';

function maskVideo(){

    if( !video.paused ){

        bufferCtx.drawImage( mask, 0, 0 );

        const data = bufferCtx.getImageData( 0, 0, w, h );

        for( let i = 0; i < data.data.length; i += 4 ){

            data.data[i+3] = data.data[i];

        }

        outputCtx.putImageData( data, 0, 0 );
        outputCtx.drawImage( video, 0, 0, w, h );

        FPSCount++;

    } else if( FPSCount ){

        fpsOutput.textContent = (FPSCount / video.duration * video.playbackRate).toFixed(2);
        FPSCount = 0;

    }

    window.requestAnimationFrame( maskVideo );

}

window.requestAnimationFrame( maskVideo );
html {
    font-size: 1vh;
    font-size: calc(var(--scale,1) * 1vh);
}
html, body {
    height: 100%;
}
body {
    padding: 0;
    margin: 0;
    overflow: hidden;
}
video {
    display: none;
}
canvas {
    position: fixed;
    top: 0;
    left: 0;
}
#fps {
    position: fixed;
    top: 10px;
    left: 10px;
    background: red;
    color: white;
    z-index: 5;
    font-size: 50px;
    margin: 0;
    font-family: Courier, monospace;
    font-variant-numeric: tabular-nums;
}
#fps:after {
    content: 'fps';
}
<video id="video" src="./video.mp4" preload muted hidden></video>
<video id="mask" src="./mask.mp4" preload muted hidden></video>
<canvas id="output"></canvas>
<canvas id="buffer" hidden></canvas>

<p id="fps" hidden></p>

一个技巧是在较小版本的视频上做色键。
通常在视频上,您不会真正注意到掩蔽的质量低于实际可见视频。然而 CPU 会注意到它要比较的像素要少得多。
您甚至可以通过在蒙版上应用一个小的模糊滤镜来平滑边缘:

(async() => {
const video = document.getElementById( 'video' );
const mask = document.getElementById( 'mask' );
const buffer = document.getElementById( 'buffer' );
// since we're going to do a lot of readbacks on the context
// we should let the browser know,
// so that it doesn't move it from and to the GPU every time
// For this we use the { willReadFrequently: true } option
const bufferCtx = buffer.getContext( '2d', { willReadFrequently: true } );
const output = document.getElementById( 'output' );
const outputCtx = output.getContext( '2d' );


await video.play();
await mask.play();

const w = video.videoWidth;
const h = video.videoHeight;
output.width  = w;
output.height = h;
buffer.width  = mask.videoWidth  / 5; // do the chroma on a smaller version
buffer.heigth = mask.videoHeight / 5;

function maskVideo(){

    if( !video.paused ){

        bufferCtx.drawImage( mask, 0, 0, buffer.width, buffer.height );

        const data = bufferCtx.getImageData( 0, 0, buffer.width, buffer.height );

        for( let i = 0; i < data.data.length; i += 4 ) {
            data.data[i+3] = data.data[i];
        }
        // we put the new ImageData back on buffer
        // so that we can stretch it on the output context
        // alternatively we could have created an ImageBitmap from the ImageData
        bufferCtx.putImageData( data, 0, 0 );
        outputCtx.clearRect(0, 0, w, h);
        outputCtx.filter = "blur(1px)"; // smoothen the mask
        outputCtx.drawImage( buffer, 0, 0, w, h );
        outputCtx.globalCompositeOperation = 'source-in';
        outputCtx.filter = "none";
        outputCtx.drawImage( video, 0, 0, w, h );
        outputCtx.globalCompositeOperation = 'source-over';

    }

    window.requestAnimationFrame( maskVideo );

}

window.requestAnimationFrame( maskVideo );
})();
<video id="video" src="https://dl8.webmfiles.org/big-buck-bunny_trailer.webm" preload muted hidden loop></video>
<!-- not a great mask video example since it's really in shades of grays -->
<video id="mask" preload muted loop hidden src="https://upload.wikimedia.org/wikipedia/commons/6/64/Plan_9_from_Outer_Space_%281959%29.webm" crossorigin="anonymous"></video>
<canvas id="output"></canvas>
<canvas id="buffer" hidden></canvas>

另一个解决方案,因为你的蒙版是白色和黑色的,所以使用 SVG 滤镜,特别是 <feColorMatrix type=luminanceToAlpha>。这应该将所有工作留在 GPU 端,并消除对中间缓冲区的需要 canvas,但请注意,Safari 浏览器仍然不支持该选项...

(async() => {
const video = document.getElementById( 'video' );
const mask = document.getElementById( 'mask' );
const output = document.getElementById( 'output' );
const outputCtx = output.getContext( '2d' );


await video.play();
await mask.play();

const w = output.width  = video.videoWidth;
const h = output.height = video.videoHeight;

function maskVideo(){

    outputCtx.clearRect(0, 0, w, h);
    outputCtx.filter = "url(#lumToAlpha)";
    outputCtx.drawImage( mask, 0, 0, w, h );
    outputCtx.filter = "none";
    outputCtx.globalCompositeOperation = 'source-in';
    outputCtx.drawImage( video, 0, 0, w, h );
    outputCtx.globalCompositeOperation = 'source-over';

    window.requestAnimationFrame( maskVideo );

}

window.requestAnimationFrame( maskVideo );
})();
<video id="video" src="https://dl8.webmfiles.org/big-buck-bunny_trailer.webm" preload muted hidden loop></video>
<video id="mask" preload muted loop hidden src="https://upload.wikimedia.org/wikipedia/commons/6/64/Plan_9_from_Outer_Space_%281959%29.webm" crossorigin="anonymous"></video>
<canvas id="output"></canvas>
<svg width=0 height=0 style=visibility:hidden;position:absolute>
  <filter id=lumToAlpha>
    <feColorMatrix type="luminanceToAlpha" />
  </filter>
</svg>