目录
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使用行优先)
大致流程
-
Base64 → 字节数组
-
字节数组 → UTF-8字符串
-
解析JSON得到RLE对象
-
解码RLE得到掩码数组
-
转换为ImageData


被折叠的 条评论
为什么被折叠?



