前端实时渲染性能优化 使用cocoRLE编码进行图像传输并着色绘制

2025博客之星年度评选已开启 10w+人浏览 699人参与

目录

​编辑

优化背景

cocoRLE编码

RLE编码原理

cocoRLE解码并着色绘制的过程

代码解析

数据格式说明

rleFrString核心RLE解码算法

decodeCocoRle(cocoRle) - 核心解码函数

大致流程


优化背景

需要多帧实时绘制mask图像,每帧的mask需要遍历每个像素点,判断其rgb是否为0~255之间的任一数值并替换相应的颜色,mask的数据是base64编码,如果要提前存储下来需要完整像素数组和对应的颜色的话,那么对于内存的占用是极高的,不切实际,所以对于快速切帧时,需要每次都遍历一遍整个图像的像素点改变其颜色,cpu就会拉满,页面会比较卡顿并且mask的绘制会有延迟,就算将这个操作放在worker中去计算,改变也是微乎其微的。

对于实时渲染的场景,渲染的流畅性直接决定用户的体验。

cocoRLE编码

COCO RLE编码是对mask进行行程长度编码,好处是压缩和解压缩都非常快,它是二值掩码(只有0和1),RLE能极大减少存储空间,并且有一点很重要:它可以解码时直接处理,无需完整存储像素数组,在解码时直接赋值颜色!

RLE编码原理

原始掩码: [0,0,0,1,1,1,0,0,1,1]
RLE计数: [3,3,2,2]  // 表示:3个0, 3个1, 2个0, 2个1

cocoRLE解码并着色绘制的过程

拿Python用pycocotools库 生成的cocoRle编码举例

        // ===================== 核心解码函数 =====================
        function base64ToUint8Array(base64) {
            const binaryString = atob(base64);
            const len = binaryString.length;
            const bytes = new Uint8Array(len);
            for (let i = 0; i < len; i++) bytes[i] = binaryString.charCodeAt(i);
            return bytes;
        }

        // 从压缩字符串解码 RLE counts
        function rleFrString(s, h, w) {
            let m = 0;
            let p = 0;
            let k;
            let x;
            let more;
            let cnts = [];
            
            while (s[p]) {
                x = 0;
                k = 0;
                more = 1;
                while (more) {
                    const c = s.charCodeAt(p) - 48;
                    x |= (c & 0x1f) << (5 * k);
                    more = c & 0x20;
                    p++;
                    k++;
                    if (!more && c & 0x10) {
                        x |= -1 << (5 * k);
                    }
                }
                if (m > 2) {
                    x += cnts[m - 2];
                }
                cnts[m++] = x;
            }
            return cnts;
        }

        function parseCocoRleStr(rleStr) {
            // 先处理双引号格式的字节字符串 b"..."
            let jsonStr = rleStr.replace(/b"([^"]*)"/g, '"$1"');
            // 再处理单引号格式的字节字符串 b'...'
            jsonStr = jsonStr.replace(/b'([^']*)'/g, '"$1"');
            // 处理其他 Python 格式
            jsonStr = jsonStr.replace(/'/g, '"').replace(/None/g, 'null');
            const rleObj = JSON.parse(jsonStr);
            return { size: rleObj.size, counts: rleObj.counts };
        }

        function decodeCocoRle(cocoRle) {
            const [height, width] = cocoRle.size;
            const counts = rleFrString(cocoRle.counts, height, width);
            const maskFOrder = new Uint8Array(height * width);
            let idx = 0;
            let v = false;

            for (let j = 0; j < counts.length; j++) {
                for (let k = 0; k < counts[j]; k++) {
                    if (idx < maskFOrder.length) {
                        maskFOrder[idx++] = v ? 255 : 0;
                    }
                }
                v = !v;
            }

            // F序转行优先
            const maskRowMajor = new Uint8Array(height * width);
            for (let row = 0; row < height; row++) {
                for (let col = 0; col < width; col++) {
                    const fIdx = col * height + row;
                    const rowIdx = row * width + col;
                    maskRowMajor[rowIdx] = maskFOrder[fIdx];
                }
            }
            return { mask: maskRowMajor, width, height };
        }

        function maskToImageData(mask, width, height) {
            const data = new Uint8ClampedArray(width * height * 4);
            for (let i = 0; i < mask.length; i++) {
                data[i * 4] = mask[i];
                data[i * 4 + 1] = mask[i];
                data[i * 4 + 2] = mask[i];
                data[i * 4 + 3] = 255;
            }
            return new ImageData(data, width, height);
        }

        async function decodeRleFromPython(base64Rle) {
            const rleBytes = base64ToUint8Array(base64Rle);
            const rleStr = new TextDecoder('utf-8').decode(rleBytes);
            const cocoRle = parseCocoRleStr(rleStr);
            const { mask, width, height } = decodeCocoRle(cocoRle);
            const imageData = maskToImageData(mask, width, height);
            return { mask, width, height, imageData };
        }

        // 替换成你从Python拿到的Base64编码字符串
        const base64Rle = "eydzaXplJzogWzE1MzYsIDE5MjBdLCAnY291bnRzJzogYidoZ2NmMDZmXzE1TjEwMU8wTzJPMDAyTjBPMkk2TzEwME8yTzBPMU8xTzJPMDAwTzEwMDAwMDFOMTAwMDFOMk8wTzEwMU8wTzEwMDAwMDAwMDAwTzEwMDAxTzAwMDAxTjEwMDAwMDBPMU8xMDBPMTAwMDAwMDAwMTAwTzJONUsxTzFPMU8wMDFPMU8xTzFPMk4yTzBPMU8xTzFPaE5YT1pjTmYwZVxcMVxcT1xcY05iMGNcXDFAXWNOP2NcXDFBXWNOP2JcXDFBYGNOPmBcXDFCYGNOPl9cXDFDYWNOPl1cXDFCZWNOPVpcXDFEZmNOPFpcXDFDZ2NOPVlcXDFCaWNOPVhcXDFAaWNOYTBrXTEwMDFPMDAwMDAwMDAwMDFPMDAwMDAwMDAwMDAwME8xMDAwME8xMDAwMDAwTzFPMk4xMDBPMTAwTzFhTlZPZ2NOazBWXFwxXFxPZWNOZTBXXFwxRGFjTj9dXFwxRl1jTj1iXFwxSVdjTjloXFwxTFJjTjZtXFwxMGtiTjNVXTFcXDEwMDAwTzEwMDAwMDAwMDAxTzAwMDAwMDFPMDAxTzAwMDAwMDAwMDAwMDAwMDAxTzAwMDAwMDFPMDAwMDAwMDFPMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxTzAwMDAwMDAwMDAwMDAwMDAwMDFPMDAwMDAwMDAwMU8wMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMU8wMU8wMU8wMDFPMU8xTzFPMU8xTzAwMU8wMDAwMU8wMDFPMDAxTzFPMTBPMDFPMU8yTjFPMU8wMDAwMDAxTzAwMk4zTTRMOEgzTTNNMk4xMDBPMU8yTjJOMk4xTzAwMU8wMDEwTzAwMDFPMDAwMDAwMDAwMDAwMDAwMDAwMDAxTzAwTzEwMDAwMDAwMU4xMDAwMDAwMDBPMTAxTjEwME8xTzJOMU8yTjFPMkw0TTNGVk5bYk5sMWRdMTcwMU8wMDFPMU8xTzAxMDAwMU8wMDAwMDAxTzAwTzFPMk4xTzEwME8xMDFPME8xMDAwTzAxMDAwME8xMDFOMTAwMDBPMTAwMDBPMTAwMDAwMDAxTzAwMDAwTzEwMDAwTzFPMU8xMDBPMTAwMDAwMDAwTzEwMDAwMDFPMDAwMU8wMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxTzAwMDAwTzEwMDAwMDAwME8xMDBPMU8xTzFPMU8xTzEwME8xMDAwMDAwMDFPMDAwTzEwME8xMDBPMTAwMDAwMDAxTzAwMDAwMDAwMDEwTzAwMDBPMTAwMDAwMDAwMDAwMDFPMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMU8wMU8wMDAwMDAxTzAwMDEwTzAwMDAxTzAwMU8wMDAwMU8wMDAwMDAwMDFPMDExTjJOMU8yTjEwME8wMDFPMTBPMDAwMU8wMDAxTzAxTzAwMDAxME8wMDAwMDFPMDAwMDAwME8xMDAwMDBPMk8wMDAwME8xMDBPMTAwMDBPMU8xMDBPMTAwTzFPMU8xTzFPMTAwTzEwMDAwMDAwMDAwMU8wMDAwMDAwMDAwMU8wMDAwMDAwMDAwMDAwMU8wTzEwMDAwMDAwMDAwMDAwMDAwTzEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMU8wME8xMDFPMDAwMDBPMTAwMU8wMDAwMU8wMDAwMU8wMDAwMDAwMU8wMDAwMDAxTzAwMDAwMDAwMDFPMDAwMDAxTzAwMTBPMDFPMTAxTk4zTzFOMk8xTjNOMU4zTjFPMU8xTzFPMU8xTzFPMU8xTzAwMTAwTzAwMU8wMDAwMU8wMDFPMU8wX05lYk49XFxdMUFmYk4+W10xQmNiTj9dXTFSMTFPMk4xTzFPMDBPMU8xTTNGOl1PW01pY05nMlNcXDFhTWljTl8yVlxcMWRNZ2NOazBPM1pcXDFTT2ZjTmgwMjVZXFwxUU9mY05qMDE1XVxcMWxOaGNOaTBLPVRdMUBqYk5iMGtcXDFgTlxcY05sMEhlMGtcXDFhTl5jTmgwRl1PMFAxbFxcMWtOX2NOOkZMT04xUTFrXFwxa05gY044R01NTzJQMWpcXDFsTmBjTjhGTk1PM1AxWV0xV09kYk42MWQwWV0xWzFOMks1TjIwMDAwMDAwMDFPMVhNUWNOXTJaXTFYTmJiTk1POGFdMURoYk4wSTtgXTFBa2JOM0U8YV0xQGtiTjNEPWNdMV9PaWJOM0Q+Zl0xXU9lYk42Qz5pXTFdT2JiTjVFPmldMUBfYk5QMWJdMVFPXmJObjBjXTFTT1tiTm0wZV0xU09bYk5tMGVdMVJPXFxiTm4wZF0xUE9eYk5RMWNdMWtOX2JOVTFXXjFPbk5rTlpjTlUxZFxcMW1OXmNOUDFiXFwxUE9gY05vMF9cXDFRT19jTlExYVxcMW9OWGNOWDFoXFwxaE5VY05bMWtcXDFlTlBjTmAxUV0xXU5uYk5lMVNdMVhOZmJOUjJaXTFsTWdiTlUyWl0xak1mYk5WMlhdMTowME8xMDAwMDAwMDAwMDAwMU8xTzJONkoyTjFPMk4zTTJOMk4zTjFOMU8yTjJOMU8xTzFPMU8xTzFPMDAxTzAwMU4xMDFPMDAxTzAwMU8wMU8wMDAxTzAwMDAwMDAxTzAwME8xMDAxTzAwMDAwMDFPMDFPMDAwMDAwMDAwMDAwMDAwTzIwTzAwMDAwMDEwTzAwMDAwMDAwMDAwMDAwMDBPMTAwMTBPMDFPMk4wMDFPMDFPTzAxMDBPMU4yMDBPMTAwMDAwTzEwMU8wZU5oTm5jTlgxUVxcMWpObWNOVzFAZ05qWzEyZWROWDFBZk5qWzEzYGROWzFZXFwxZU5mY05cXDFaXFwxZU5kY05bMV1cXDFmTmpiTktgMGAxZlxcMW1OWGNOVDFoXFwxbk5UY05UMWxcXDFtTm9iTlcxUV0xaU5tYk5ZMVNdMWdOamJOXFwxVl0xZE5pYk5dMVddMWNOaGJOXzFvMFJOU1sxP25jTl8xbjBWTlFbMTtRZE5gMWwwWU5QWzE3U2ROYjFsMFlOb1oxNVVkTmMxYTBQTkI5Z1sxNVRkTmIxZDBRTl9POWpbMVcyZ2ROY01cXE83b1sxUzJlZE5qTV9PTW1bMVgyY2RObE1BS21bMVgyX2ROUE5FR2xbMVkyXWROUk5HRWxbMVgyXWROVE5IQ2xbMVkyW2ROVE5JR2lbMVQyYGROU05HTmZbMW0xZmROUk5EMmZbMWsxaGROUU5DNGVbMWsxZ2ROUU5ENGVbMWwxZ2ROb01FNWNbMWwxaGROb01FNWRbMWwxZmROUE5FNGVbMWwxZmROaE5dWzFVMWNkTmxOXlsxUjFgZE5QT0RrTmZbMVYyXmROXk9iWzFgMjFPMGlNVWRON21bMW4xMDAwMDEwMU8wZk1SZE4/blsxQFJkTmEwblsxXk9SZE5iMFBcXDFcXE9QZE5lMFBcXDFlMTEwMTBPOEkwMDAwT08yTjJPME8xMFdNXWROVDFiWzFrTmBkTlUxYFsxak5hZE5WMV5bMWtOX2ROVzFhWzFkMTAxTzAwMDAwMDFeZE5oS1pbMVg0ZmROaEtbWzFWNGlkTmZLWFsxWjQ4TzAwMU4xMDBfT1lkTmlMZ1sxaDNPMk4xZWROaEtuWjFYNGtkTlJMUVsxYzRMME8yTzAwME8xME8wMTAwMFZMVWVOXzJqWjFiTVZlTl0ya1oxY01VZU5cXDJrWjFlTVVlTlsyaloxZk1WZU5ZMmtaMWdNVGVOWTJsWjFoTVRlTlcybVoxaU1TZU5WMm5aMWpNUmVOVTJuWjFsTVJlTlMyb1oxbU1RZU5tMVRbMVRObGROajFWWzFWTmpkTmkxU1sxW05tZE5jMVFbMWFOb2ROXjFQWzFkTlFlTlkxaFoxU01ZZU5lMU5YMWlaMVQyTzFOMTAwTjFuZU5pSl1ZMWg0VWZOXUthME1ZWTFnNG9mTl5LUFkxYTRRZ05fS1BZMWA0UmdOXktvWDFhNFNnTl1LbVgxYzRUZ05cXEttWDFjNFNnTl1LblgxYjRSZ05eS25YMWI0XFxmTltLODJdWTFiNFtmTlxcSzgyXlkxYTRaZk5dSzgxYFkxYTRYZk5eS2BaMWI0YGVOXkthWjFjND1IaGROaks1S2haMVY0WGVOWExoWjFmM1plTlpMZ1oxZTNZZU5aTGhaMWgzVmVOWExrWjFgM1BlTlhMMjJUWzFlM2xkTllMTzBYWzFkM2tkTl1MS09bWzFjM2tkTl5MSk9cXFsxYTNrZE5gTElOXlsxXFwzbmROZ0xCTmBbMWEzZ2ROaUxaWzFXM2RkTmpMXVsxbDMwTzJPME8ybE5aZE5sTWdbMVMyWWRObU1nWzFVMzJQT1lkTmRNaVsxZTFWZE5qTTFNMGQwaVsxYzFaZE5pTTBOTmQwalsxYzFZZE5qTTBPTWMwa1sxYzFaZE5pTU8wTWQwa1sxYTFaZE5rTU4xTGIwbVsxYjFZZE5sTU0wTWIwblsxYTFXZE5vTUsxTj9RXFwxYTFVZE5UT21bMWwwUWROVU9vWzFcXDIzTDNOMk8yTDNPMkNgY05XTWFcXDFoMmFjTlZNYFxcMWkyYmNOVU1gXFwxajJhY05VTV9cXDFrMjsxMDBPMU8yTjFPMDAwMU4xTzJLNVhPbWJOXk5WXTFgMW1iTl1OVl0xXjFvYk5fTlNdMV4xUGNOYU5SXTFSMWBiTm9OP09RXTFRMWFiTlBPP0xTXTFTMV5iTlBPYTBKVF0xVDFdYk5RT1peMW4wZ2FOUE9bXjFuMGdhTlBPWl4xbzA9TTNOMk4yTzJMNEw0TTNNNEw0TTJNX2ZTZTAnfQ==";

        // ===================== 执行解码并显示 =====================
        decodeRleFromPython(base64Rle).then(result => {
            const canvas = document.getElementById('maskCanvas');
            canvas.width = result.width;
            canvas.height = result.height;
            const ctx = canvas.getContext('2d');
            ctx.putImageData(result.imageData, 0, 0);
            
            // 控制台输出解码后的掩码数组
            console.log("解码后的掩码数组(行优先):", result.mask);
            console.log("掩码尺寸:", result.width, "x", result.height);
        }).catch(err => {
            console.error("解码失败:", err);
        });

代码解析

数据格式说明

输入Base64解码后的JSON结构

{
  "size": [1536, 1920],  // 高度, 宽度
  "counts": "hgc...f"    // RLE编码字符串
}

rleFrString核心RLE解码算法

function rleFrString(s, h, w) {
    let m = 0;     // 输出数组索引
    let p = 0;     // 输入字符串索引
    let k;
    let x;
    let more;
    let cnts = []; // 存储RLE计数的数组
    
    while (s[p]) {
        x = 0;
        k = 0;
        more = 1;
        while (more) {
            const c = s.charCodeAt(p) - 48;  // ASCII '0'=48
            x |= (c & 0x1f) << (5 * k);      // 取低5位,左移
            more = c & 0x20;                 // 检查第6位是否为1(继续标志)
            p++;
            k++;
            if (!more && c & 0x10) {         // 检查符号位
                x |= -1 << (5 * k);
            }
        }
        if (m > 2) {
            x += cnts[m - 2];                // 差分编码:当前值=前前值+增量
        }
        cnts[m++] = x;
    }
    return cnts;
}
  • 使用6位编码:每个字符编码5位数据 + 1位继续标志

  • 差分编码:存储的是与前前值的差值,不是绝对值

  • 为了进一步压缩数据

decodeCocoRle(cocoRle) - 核心解码函数

function decodeCocoRle(cocoRle) {
    const [height, width] = cocoRle.size;
    const counts = rleFrString(cocoRle.counts, height, width);
    const maskFOrder = new Uint8Array(height * width);
    let idx = 0;
    let v = false;  // 当前填充值:false=0, true=1

    for (let j = 0; j < counts.length; j++) {
        for (let k = 0; k < counts[j]; k++) {
            if (idx < maskFOrder.length) {
                maskFOrder[idx++] = v ? 255 : 0;
            }
        }
        v = !v;  // 交替切换0和1
    }

    // F序(列优先)转行优先
    const maskRowMajor = new Uint8Array(height * width);
    for (let row = 0; row < height; row++) {
        for (let col = 0; col < width; col++) {
            const fIdx = col * height + row;      // 列优先索引
            const rowIdx = row * width + col;     // 行优先索引
            maskRowMajor[rowIdx] = maskFOrder[fIdx];
        }
    }
    return { mask: maskRowMajor, width, height };
}
  • COCO RLE使用列优先(Fortran顺序) 存储

  • RLE计数交替表示0和1的长度

  • 需要从列优先转换为行优先(Canvas使用行优先)

大致流程

  1. Base64 → 字节数组

  2. 字节数组 → UTF-8字符串

  3. 解析JSON得到RLE对象

  4. 解码RLE得到掩码数组

  5. 转换为ImageData

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山楂树の

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值