前言
在上一篇文章《前端分割实现》中,初步实现了前端分割碎片的功能。但是UI给过来的图片在蓝湖上出现了破损,导致边缘和圆角的RGBA不精确,导致采样失败。
虽然后续让UI直接把PS的图片导出发给前端,解决了图片质量问题。
但中间还是通过连通域优化,解决了问题。
需要记录一下。
解决的思路来自这篇文章:《# OpenCV 笔记(13):连通域分析》
思路
解决思路很简单:
1、在根据RGBA的值,得到目标图片数据
2、进行连通域标记
3、如果连通域面积小于指定阈值,则把该连通域的RGBA数据移除,比如把不透明度设置为0
4、剩下的是有效数据,对有效数据进行裁剪。
代码如下
/**
* 裁剪
* @param index 序号
* @param list 存储列表
*/
cut(index: number, list: any[]) {
const ctx = this.ctx
const canvas = this.canvas
const newData = ctx.createImageData(this.width, this.height);
for (let i = 0; i < newData.data.length; i += 4) {
const temp = this.tempData.data.slice(i, i + 4)
if (Math.abs(temp[3] - index * 13) < 1) {
newData.data[i + 0] = this.imgData.data[i + 0]
newData.data[i + 1] = this.imgData.data[i + 1]
newData.data[i + 2] = this.imgData.data[i + 2]
newData.data[i + 3] = this.imgData.data[i + 3]
}
}
// 进行连通域标记
const labels = this.connectedComponentLabeling(newData.data, this.width, this.height);
const areaCounts = this.countAreas(labels, this.width, this.height);
// 设定一个阈值来移除小的连通域
const thresholdArea = 100; // 设定一个合理的阈值
// 应用面积过滤
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
const index = y * this.width + x;
const label = labels[index];
if (areaCounts[label] < thresholdArea) {
newData.data[index * 4 + 3] = 0; // 移除小面积区域
}
}
}
// 重新确定边界
let minX = this.width;
let minY = this.height;
let maxX = -1;
let maxY = -1;
// 遍历所有像素,查找非透明像素的位置
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
const pixelIndex = (y * this.width + x) * 4;
if (newData.data[pixelIndex + 3] > 0) { // Alpha通道大于0表示非透明
minX = Math.min(minX, x);
maxX = Math.max(maxX, x);
minY = Math.min(minY, y);
maxY = Math.max(maxY, y);
}
}
}
// 如果没有找到非透明像素,则直接返回
if (minX === this.width && minY === this.height) {
console.log('No non-transparent pixels found.');
return;
}
const width = maxX - minX + 1;
const height = maxY - minY + 1;
canvas.width = width;
canvas.height = height;
// 创建新的图像数据对象用于裁剪后的图像
const croppedImage = ctx.createImageData(width, height);
// 复制非透明像素到新的图像数据中
for (let y = minY; y <= maxY; y++) {
for (let x = minX; x <= maxX; x++) {
const srcIndex = ((y * this.width) + x) * 4;
const destIndex = ((y - minY) * (maxX - minX + 1) + (x - minX)) * 4;
croppedImage.data.set(newData.data.subarray(srcIndex, srcIndex + 4), destIndex);
}
}
// 清除画布并绘制裁剪后的图像
ctx.clearRect(0, 0, width, height);
ctx.putImageData(croppedImage, 0, 0);
const dataUrl = canvas.toDataURL('image/png');
list.push({
x: minX,
y: minY,
width,
height,
dataUrl
});
},
/**
* 对给定的图像数据执行连通域标记算法。
* @param {Uint8ClampedArray} data 图像数据。
* @param {number} width 图像宽度。
* @param {number} height 图像高度。
* @returns {Array<number>} 包含每个像素所属连通域标签的一维数组。
*/
connectedComponentLabeling(data: Uint8ClampedArray, width: number, height: number) {
const labels = new Array(width * height).fill(0); // 初始化标签数组
let nextLabel = 1; // 下一个可用的标签值
function dfs(x: number, y: number, label: any) {
const stack: any = [[x, y]];
while (stack.length > 0) {
const [cx, cy] = stack.pop();
if (cx < 0 || cy < 0 || cx >= width || cy >= height || data[cy * width * 4 + cx * 4 + 3] === 0 || labels[cy * width + cx] !== 0) {
continue;
}
labels[cy * width + cx] = label;
stack.push([cx - 1, cy]); // 左
stack.push([cx + 1, cy]); // 右
stack.push([cx, cy - 1]); // 上
stack.push([cx, cy + 1]); // 下
}
}
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const index = y * width + x;
if (data[index * 4 + 3] > 0 && labels[index] === 0) { // 如果当前像素不是透明且未标记
dfs(x, y, nextLabel); // 标记连通域
nextLabel++;
}
}
}
return labels;
},
/**
* 计算每个连通域的面积。
* @param {Array<number>} labels 每个像素所属连通域的标签数组。
* @param {number} width 图像宽度。
* @param {number} height 图像高度。
* @returns {Object} 包含每个连通域面积的对象。
*/
countAreas(labels: Array<number>, width: number, height: number) {
const areaCounts: any = {};
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const index = y * width + x;
const label = labels[index];
if (label > 0) {
areaCounts[label] = (areaCounts[label] || 0) + 1;
}
}
}
return areaCounts;
}