【前端笔记】200行代码用canvas实现视频抠图(基于颜色)

自从大二初次接触前端以来,一直都有记markdown笔记的习惯.又因为掘金最近有一个活动.所以开了这个专栏。我会写一些业务相关的小技巧和前端知识中的重点内容之类的整理一下发上来。难免有点地方不够细致。欢迎大家指教

这个文章讲一下 怎么用canvas 实现视频的颜色抠图.最后你会实现下图效果

在看代码之前,我们可以了解一下我们需要做什么

html

  1. 创建了一个包含虚拟背景效果的容器 .background-processing-container。在容器内部,包括两个 <video> 元素,一个 <canvas> 元素,以及一些控制元素。
  2. <canvas> 元素上,通过绘制背景图像来实现虚拟背景效果。
  3. 通过 <video> 元素播放原始的摄像头捕获的视频,以及另一个 <video> 元素用于播放经过虚拟背景效果处理后的视频。
  4. 使用一个输入元素 <input> 允许用户选择背景颜色,以及一个容差值输入元素来控制虚拟背景效果的敏感度。

接下来,我们来看JavaScript 部分的代码

  1. 定义了一些常量,包括视频和画布的宽高(480x300)。声明了一些全局变量,用于存储原始视频、画布、图像数据,以及虚拟视频的元素和上下文。
  2. 监听背景颜色选择框的变化事件,以便根据用户选择的颜色更改虚拟背景效果的背景颜色。
  3. 异步函数 getBackgroundImageData 用于获取背景图像数据,并将其存储在全局变量 backgroundImageData 中。
  4. drawVideoToCanvas 函数用于将原始视频帧绘制到画布上,并在后台处理视频帧,最后将处理后的帧绘制到虚拟视频画布上。
  5. processFrameDrawToVirtualVideo 函数用于处理真实视频的图像数据,根据背景颜色和容差值,将需要处理的像素替换为背景图像中的对应像素。
  6. colorDiff 函数用于计算颜色差异,以确定是否需要进行背景替换。
  7. hexToRgb 函数用于将十六进制颜色代码转换为RGB数组。
  8. 初始化背景颜色、容差值和背景颜色的全局变量。
  9. 调用 start 函数,依次执行三个重要的步骤: a. 获取背景图像数据。 b. 获取原始摄像头视频流并显示在页面上。 c. 处理视频并将处理后的视频流显示在虚拟视频元素上。

总结来说,就是我们页面有一个 初始的canvas,这个canvas需要根据容差的值进行像素的替换来实现背景抠图的功能

背景图像数据获取

主要目的是得到初始的 canvas数据

javascript 复制代码
 function getBackgroundImageData() {
            return new Promise((resolve) => {
                const backgroundCanvas = document.querySelector('#backgroundImg')
                const backgroundCtx = backgroundCanvas.getContext('2d')
                const img = new Image()
                // img.src = backgroundImg
                img.src = 'background.png'
                img.setAttribute('crossOrigin', '')
                img.onload = () => {
                    backgroundCtx.drawImage(
                        img,
                        0,
                        0,
                        backgroundCanvas.width,
                        backgroundCanvas.height,
                    )
                    //  用于合成事件
                    backgroundImageData = backgroundCtx.getImageData(
                        0,
                        0,
                        backgroundCanvas.width,
                        backgroundCanvas.height,
                    )
                    resolve(0)
                }
            })
        }

处理目标canvas和 初始canvas的 容差(工具方法)

主要是把 真实 canvas和 容差值 做一个对应关系。假如在容差值差之內,那么我们就进行背景的替换,这样来实现背景抠图

scss 复制代码
function processFrameDrawToVirtualVideo() {
            // 逐像素计算与要处理的目标颜色的差值,如果差值小于阈值,则将该像素设置为背景图片中的对应像素
            for (let i = 0; i < realVideoImageData.data.length; i += 4) {
                const r = realVideoImageData.data[i]
                const g = realVideoImageData.data[i + 1]
                const b = realVideoImageData.data[i + 2]
                const a = realVideoImageData.data[i + 3]
                const bgR = backgroundImageData.data[i]
                const bgG = backgroundImageData.data[i + 1]
                const bgB = backgroundImageData.data[i + 2]
                const bgA = backgroundImageData.data[i + 3]

                // 计算与背景色的差值
                const diff = colorDiff([r, g, b], backgroundColor)
                // 当差值小于设定的阈值,则将该像素设置为背景图片中的对应像素
                if (diff < allowance) {
                    realVideoImageData.data[i] = bgR
                    realVideoImageData.data[i + 1] = bgG
                    realVideoImageData.data[i + 2] = bgB
                    realVideoImageData.data[i + 3] = bgA
                }
            }
            // 将处理后的图像数据写到虚拟视频的 canvas 中
            virtualVideoCtx.putImageData(realVideoImageData, 0, 0)
        }

        // 计算颜色差异
        function colorDiff(rgba1, rgba2) {
            let d = 0
            for (let i = 0; i < rgba1.length; i++) {
                d += (rgba1[i] - rgba2[i]) ** 2
            }
            return Math.sqrt(d)
        }

        // 十六进制转 rgb
        function hexToRgb(hex) {
            const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
            console.log("hexToRgb:" + hex)
            return result
                ? [                    parseInt(result[1], 16),
                    parseInt(result[2], 16),
                    parseInt(result[3], 16),
                ]
                : null
        }

绘制到初始画布

我们经过上面的两个工具方法,我们已经拿到了原始的canvas 和 canvas 替换的 主逻辑。最后我们就要把这些逻辑组装一下就好了

这里就没什么好说的了,主要是 用 setInterval 把最终的 canvas 的 imageData 渲染到 页面上面

ini 复制代码
function drawVideoToCanvas(realVideo) {
    // realVideo 是 设想
    // 摄像头的canvas
    realVideoCanvas = document.createElement('canvas')
    realVideoCtx = realVideoCanvas.getContext('2d')

    virtualVideoCanvas = document.createElement('canvas')
    virtualVideoCtx = virtualVideoCanvas.getContext('2d')

    realVideoCanvas.width = virtualVideoCanvas.width = WIDTH
    realVideoCanvas.height = virtualVideoCanvas.height = HEIGHT

    // 每隔 100ms 将真实的视频写到 canvas 中,并获取视频的图像数据
    setInterval(() => {
        // 下面的人物
        realVideoCtx.drawImage(
            realVideo,
            0,
            0,
            realVideoCanvas.width,
            realVideoCanvas.height,
        )
        // 渲染图片
        realVideoImageData = realVideoCtx.getImageData(
            0,
            0,
            realVideoCanvas.width,
            realVideoCanvas.height,
        )
        // 处理真实视频的图像数据,将其写到虚拟视频的 canvas 中
        processFrameDrawToVirtualVideo()
    }, 50)
    // 从 VirtualVideoCanvas 中获取视频流并在 virtualVideo 中播放
    virtualVideo = document.querySelector('#virtual-video')
    const stream = virtualVideoCanvas.captureStream(30)
    // 重要,从这里canvas 变成最终的流
    virtualVideo.srcObject = stream
}

完整代码

xml 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .background-processing-container {
            height: 100%;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            gap: 50px;
        }
        .source {
            display: flex;
            justify-content: space around;
            gap: 50px;
            align-items: center;
            text-align: center;
        }
        video,
        canvas {
            width: 480px;
            height: 300px;
            border: 4px solid #374685;
        }
    </style>
</head>

<body>
    <!-- 
        主要原理是通过 canvas 将视频中的每一帧画到画布上,然后将画布中的像素逐个与设定的背景色(默认是簇色,你可以更换为任意符合你背景的颜色)进行计算,比较后的差值达到设定的阈值时,对其进行处理,将其更换为预先准备好的背景图的图像数据,最后将处理后的图像数据再画到虚拟背景画布上,通过虚拟背景画布拿到媒体流后给到 video 标签播放, 这样就实现了视频的虚拟背景效果。
        下面我们来看看具体的实现。
        为保持大小一致,这里我们统一设置画布和视频的宽高为 480*300
    -->
    <div class="background-processing-container">
        <canvas id="backgroundImg" width="480" height="300" class="background-img"></canvas>

        <video id="real-video" width="480" height="300" autoplay muted></video>
        <video id="virtual-video" width="480" height="300" autoplay muted></video>

        <div class="control">
            你的背景色:
            <input id="color" type="color" />
            容差值:
            <!-- <el-input-number v-model="allowance" :step="1" step-strictly /> -->
            <!-- <el-slider v-model="allowance" :max="300" :step="1" /> -->
        </div>
    </div>


    <script>
        // import backgroundImg from '@/assets/background2.png'

        const WIDTH = 480
        const HEIGHT = 300

        // 原本的视频
        let realVideo
        let realVideoCanvas
        let realVideoCtx
        let realVideoImageData

        // 虚拟的视频
        let virtualVideo
        let virtualVideoCanvas
        let virtualVideoCtx

        document.querySelector('#color').onchange = function (e) {
            // console.log(hexToRgb(e.target.value))
            backgroundColor = hexToRgb(e.target.value) ? hexToRgb(e.target.value) : "#000000"
        }

        // 重要:第一步,获取背景图的信息
        let backgroundImageData
        // 获取背景图像数据
        function getBackgroundImageData() {
            return new Promise((resolve) => {
                const backgroundCanvas = document.querySelector('#backgroundImg')
                const backgroundCtx = backgroundCanvas.getContext('2d')
                const img = new Image()
                // img.src = backgroundImg
                img.src = 'background.png'
                img.setAttribute('crossOrigin', '')
                img.onload = () => {
                    backgroundCtx.drawImage(
                        img,
                        0,
                        0,
                        backgroundCanvas.width,
                        backgroundCanvas.height,
                    )
                    //  用于合成事件
                    backgroundImageData = backgroundCtx.getImageData(
                        0,
                        0,
                        backgroundCanvas.width,
                        backgroundCanvas.height,
                    )
                    resolve(0)
                }
            })
        }


        // 合成视频
        function drawVideoToCanvas(realVideo) {
            // realVideo 是 设想
            // 摄像头的canvas
            realVideoCanvas = document.createElement('canvas')
            realVideoCtx = realVideoCanvas.getContext('2d')

            virtualVideoCanvas = document.createElement('canvas')
            virtualVideoCtx = virtualVideoCanvas.getContext('2d')

            realVideoCanvas.width = virtualVideoCanvas.width = WIDTH
            realVideoCanvas.height = virtualVideoCanvas.height = HEIGHT

            // 每隔 100ms 将真实的视频写到 canvas 中,并获取视频的图像数据
            setInterval(() => {
                // 下面的人物
                realVideoCtx.drawImage(
                    realVideo,
                    0,
                    0,
                    realVideoCanvas.width,
                    realVideoCanvas.height,
                )
                // 渲染图片
                realVideoImageData = realVideoCtx.getImageData(
                    0,
                    0,
                    realVideoCanvas.width,
                    realVideoCanvas.height,
                )
                // 处理真实视频的图像数据,将其写到虚拟视频的 canvas 中
                processFrameDrawToVirtualVideo()
            }, 50)
            // 从 VirtualVideoCanvas 中获取视频流并在 virtualVideo 中播放
            virtualVideo = document.querySelector('#virtual-video')
            const stream = virtualVideoCanvas.captureStream(30)
            // 重要,从这里canvas 变成最终的流
            virtualVideo.srcObject = stream
        }

   
        // !!!重要:合成:处理真实视频的图像数据,将其写到虚拟视频的 canvas 中
        function processFrameDrawToVirtualVideo() {
            // 逐像素计算与要处理的目标颜色的差值,如果差值小于阈值,则将该像素设置为背景图片中的对应像素
            for (let i = 0; i < realVideoImageData.data.length; i += 4) {
                const r = realVideoImageData.data[i]
                const g = realVideoImageData.data[i + 1]
                const b = realVideoImageData.data[i + 2]
                const a = realVideoImageData.data[i + 3]
                const bgR = backgroundImageData.data[i]
                const bgG = backgroundImageData.data[i + 1]
                const bgB = backgroundImageData.data[i + 2]
                const bgA = backgroundImageData.data[i + 3]

                // 计算与背景色的差值
                const diff = colorDiff([r, g, b], backgroundColor)
                // 当差值小于设定的阈值,则将该像素设置为背景图片中的对应像素
                if (diff < allowance) {
                    realVideoImageData.data[i] = bgR
                    realVideoImageData.data[i + 1] = bgG
                    realVideoImageData.data[i + 2] = bgB
                    realVideoImageData.data[i + 3] = bgA
                }
            }
            // 将处理后的图像数据写到虚拟视频的 canvas 中
            virtualVideoCtx.putImageData(realVideoImageData, 0, 0)
        }

        // 计算颜色差异
        function colorDiff(rgba1, rgba2) {
            let d = 0
            for (let i = 0; i < rgba1.length; i++) {
                d += (rgba1[i] - rgba2[i]) ** 2
            }
            return Math.sqrt(d)
        }

        // 十六进制转 rgb
        function hexToRgb(hex) {
            const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
            console.log("hexToRgb:" + hex)
            return result
                ? [
                    parseInt(result[1], 16),
                    parseInt(result[2], 16),
                    parseInt(result[3], 16),
                ]
                : null
        }
        // 初始的背景色
        let color = '#000000'
        // let temp = rgba(100,200,100)
        // setTimeout(()=>{
        //     color ='#00000'
        // },1000)
        // 重要:设置 diff 阈值
        const allowance = 162
        let backgroundColor

        // 重要:需要扣除的背景色
        backgroundColor = hexToRgb(color)
        // watch(
        //     () => color.value,
        //     (newVal) => {
        //         // 十六进制转 rgb
        //         backgroundColor = hexToRgb(newVal)
        //     },
        //     {
        //         immediate: true,
        //     },
        // )
        // 开始
        async function start() {
            // 重要第一步:在canvas绘制图像,显示出来
            await getBackgroundImageData()
            // 重要第二步:显示出来没有经过变化的原始摄像头,其实是没有什么意义的
            const stream = await navigator.mediaDevices.getUserMedia(
                {
                    video: {
                        width: WIDTH,
                        height: HEIGHT,
                    }
                })
            realVideo = document.querySelector('#real-video')
            realVideo.srcObject = stream

            // 重要第三步:这个是主要逻辑方法
            drawVideoToCanvas(realVideo)
        }
        start()
        // start()

    </script>

</body>

</html>

源码地址:gitee.com/Electrolux/...

在线演示地址:electrolux.gitee.io/front-css-p...

相关推荐
weixin_382395233 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__3 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.4 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~5 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华5 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子8 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅8 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni8 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学9 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端