前言
在开发一个AI绘图应用中,有一个功能是用户上传原图,并通过涂抹来告诉AI模型哪些地方需要重绘,哪些地方需要保留原图。下图展示了示例,左图是原图,右图中的白色高亮区域表示保留区域,黑色半透明部分表示AI重绘的部分。如果用户希望保留原图中模特的头部,就需要使用画笔涂抹原图。然而,由于毛发的复杂性较高,完整地涂抹是困难的。
为了让用户能够快速选择完整的模特头部,需要使用SAM(segment-anything Meta公司开源的图像分割模型)模型对图像进行预先分割。用户只需要鼠标点击分割区域即可选择完整的部分。
上图展示了SAM的分割效果,该模型可以根据语义将输入图像分割为不同的部分。然而,为了减少数据传输量,SAM会使用RLE格式进行数据压缩,如果要在网页实现分割图的交互,例如鼠标经过时显示高亮,这就涉及RLE解码、图像中心点计算和距离运算的知识。
本文将详细介绍RLE算法的编码和解码原理 ,以及如何利用欧氏距离计算多个分割图的高亮交互。
1. RLE编码与base64编码空间对比
刚才提到SAM模型预测后会以RLE算法压缩数据,我们通过对比压缩前后空间占用情况,搞清楚为什么需要RLE压缩。
上图是一张经过SAM模型计算并使用不同颜色标注的图像,图像包含43个分割对象。下面我分别用base64和RLE算法统计43个图像空间占用。其中base64编码总空间为1167kb ,RLE压缩占用空间为89kb,两者相差13倍。
因此,经过RLE压缩后能显著降低空间占用情况,在数据传输时会得到较大提升。
2. RLE编码
RLE(Run-Length Encoding)是一种用于将连续重复的数据序列进行编码的算法。
举个例子:
原始数据:00000011100011111 RLE编码后用数组存储:[5,3,3,5]
在上述示例中,原始数据中的0出现了5次,1出现了3次,0出现3次,1出现5次,所以记作[5,3,3,5]。
通过上面例子可以看到RLE在压缩连续数据有较大优势,但对于非连续重复数据或随机数据,效果并不好。
看下图例子,粉红色区域是SAM分割的人脸,为了更直观,我用1表示人脸区域。根据RLE压缩算法,可以表示为:[8,4,3,4,3,3,3,3,4,3,4]
3. RLE解码
为了在图像上绘制RLE数据,我们需对RLE数据解码,解码过程和第二节提到的编码反过来即可。下面还是以人脸分割的例子讲解。
**第一步:**创建一个与图像(宽7高6)尺寸一致的数组用于存放解码后数据
const pixels = new Uint8Array(7 * 6);
**第二步:**遍历压缩数据,根据记录长度还原到数组
ini
let pixelIndex = 0;
for (let i = 0; i < counts.length; i += 2) {
const zeros = counts[i];
const ones = counts[i + 1];
pixelIndex += zeros;
for (let j = 0; j < ones; j++) {
pixels[pixelIndex + j] = 1;
}
pixelIndex += ones;
}
return pixels;
结果:
对压缩数据解码:[8,4,3,4,3,3,3,3,4,3,4] 得到下图数据
4. 前端交互
接下来,我们将实现一个交互功能,当鼠标移动到分割图上时,对应区域高亮显示。
实现思路: 通过计算每个分割图的中心点坐标与鼠标坐标之间的距离,找到与鼠标距离最短的分割图显示高亮。
技术要点:
-
计算分割图的中心坐标
-
计算中心点与鼠标坐标之间的距离
分割图中心点计算公式
上图红色框假设为分割图的坐标信息,并需要计算其中心坐标。根据计算公式,将x轴和y轴的坐标累加,然后除以坐标点的数量即可。
代入公式计算得到:x = (2+3+2+3+2+3) / 6 = 2.5
代入公式计算得到:y = (6+6+5+5+4+4) / 6 = 5
因此,上图的中心坐标为 (2.5, 5)。
分割图中心点计算代码
ini
for (let j = 0; j < mask.length; j++) {
if (mask[j]) {
const x = j % width;
const y = Math.floor(j / width);
sumX += x;
sumY += y;
count++;
}
}
// 中心点
const center = { x: sumX / count, y: sumY / count }
根据计算公式,我们可以将其转换为相应的代码。const x = j % width;
用于获取每个点的x坐标,const y = Math.floor(j / width);
用于获取y坐标。sumX += x;
sumY += y;
分别表示x和y坐标的总和。
最后,将坐标的总和除以坐标数量 sumX / count
即可得到中心坐标。
距离计算
假设两个坐标点(x1,y1)为分割图中心,坐标 (x2,y2)为鼠标坐标。要计算z的距离,根据勾股定理计算z值。
因此,我们可以监听鼠标移动事件,得到鼠标坐标后逐一与分割图中心坐标计算得到距离,取得最小值作为显示目标,转换为代码如下:
ini
// dx为鼠标x轴与分割图x轴距离
const dx = x - centroids[i].x;
const dy = y - centroids[i].y;
// 根据欧式距离公式得到距离
const distance = Math.sqrt(dx * dx + dy * dy);
// 对比所有分割图,取最小值
if (distance < minDistance) {
minDistance = distance;
nearestMaskIndex = i;
}
效果展示
最后,只需要把对应分割图和原图通过Canvas绘制到页面,即可达到上图效果。
======