揭示局部奇观
前言
本文探索热门AI换图技术,深入探讨图像生成、局部替换与保留的解决方案。
图生图原理
AI 生成图的流程示意
graph LR
主图 --> 色域图 --> 蒙板图--> 局部编辑--> 蒙板图 --> 风格 --> 咒语 --> AI生成
局部编辑原理
这里只关心前端的实现,本文重点介绍局部编辑的方案。下方是整体局部编辑图层信息层级递增及数据层信息。
主图图层:展示主图图片
选区蒙板图层:绘制鼠标选中区域遮罩图
鼠标操作图层:鼠标移入移除显示遮罩区域图
色域图数据:基础色域划分图,提供遮罩图的生成规则
journey
section 可视区
主图图层: 6: 层级 1
选区蒙板图层: 4: 层级 2
鼠标操作图层: 2: 层级 3
section 数据层
色域图数据: 5: 数据层
选中色域 Map: 5: 数据层
效果图
实现步骤
graph LR
色域图 --> getImageData --> 切片 --> webworker --> 合并Map --> colorMap --> 色域图--> 选区透明图层 --> 鼠标透明图层 --> 获取鼠标位置 --> 拾取位置颜色--> 颜色获取对应Map像素集合 --> 设置选区图层颜色 --> 选区透明图层
色域图生成 colorMap
左侧为色域图,我们拿到色域图需要将其用 canvas 解析获取像素色值 imgData,由于图片的像素集合可能过于庞大。如 600*900 为例,其像素集合长度 2160000,遍历这个集合获取 colorMap 效率太低了,初步估算需要 10s 左右,还根据图片大小呈线性增长 ~
采用 webworker + 切片 效率提升 10 倍。关键代码如下~
//切片
sliceData() {
console.time("start");
const imageData = Array.from(
maskDataContext.getImageData(0, 0, this.maskWidth, this.maskHeight).data
);
//不能完成完整切割
let length = 100; //默认100份
//如果为小数,取整操作用于找出能被切割成 100 份且每一份为最大
const dataLength = parseInt(imageData.length / 4 / length) * 4;
Array.apply(null, { length }).map((i, index) => {
worker.postMessage({
type: "setColorMap",
index: (index * dataLength) / 4,
imageData: imageData.splice(0, dataLength),
});
});
//如果 data 中还有剩余的数据,则将剩余的数据单独处理
if (imageData.length > 0) {
worker.postMessage({
type: "setColorMap",
index: (length * dataLength) / 4,
imageData: imageData,
});
length++;
}
return length;
}
//颜色区域合并
mergeMapObjects(...maps) {
const color = new Map();
for (const m of maps) {
for (const item of m) {
if (color.has(item[0])) {
color.set(item[0], color.get(item[0]).concat(item[1]));
} else {
color.set(item[0], item[1]);
}
}
}
// 删除长度过少的颜色区域
for (let item of color.entries()) {
if (item[1].length < 100) {
color.delete(item[0]);
}
}
return color;
}
self.onmessage = function (e) {
let params = e.data;
let result;
if (params.type === "setColorMap") {
result = setColorMap(params);
}
self.postMessage({
type: e.data.type,
...result,
});
};
function setColorMap({ imageData, index }) {
let color = new Map();
const length = imageData.length;
for (let i = 0; i < length; i += 4) {
let key = [imageData[i], imageData[i + 1], imageData[i + 2]].join(",");
if (!color.has(key)) {
//不存在颜色 key,则添加 key
color.set(key, [i / 4 + index]);
} else {
//存在颜色 key,则修改 key 的 value
color.set(key, color.get(key).concat([i / 4 + index]));
}
}
return {
color,
};
}
//work 线程处理图片颜色区域数据
initWokrer() {
const that = this;
worker = new Worker();
worker.onmessage = function (e) {
let data = e.data;
switch (data.type) {
case "setColorMap":
that.colorMapIndex++;
that.colorMapArr = that.colorMapArr.concat(data.color);
if (that.colorMapIndex === that.sliceNum) {
colorMap = that.mergeMapObjects(...that.colorMapArr);
console.timeEnd("start");
console.log("color颜色生成完成", colorMap);
// worker.terminate();
// worker = null;
}
break;
}
};
},
绘制蒙板
有了colorMap 就可以大大减少每次鼠标拾取绘制的计算量,使得延迟系数肉眼几乎无法察觉。下一步我们需要讲拾取到的位置信息转换为色域图中的颜色信息,并通过 colorMap 拿到想要的像素位置信息,完成对鼠标操作图层及选区图层的绘制。核心代码如下
//设置颜色
setColor(isClcik) {
if (this.pickColor === this.oldColor) {
return;
}
this.oldColor = this.pickColor;
mouseContext.putImageData(transparentData, 0, 0);
//鼠标操作画布像素颜色值
const controlImgData = mouseContext.getImageData(
0,
0,
this.maskWidth,
this.maskHeight
);
//选定区域画布像素颜色值
const selectImgData = clickContext.getImageData(
0,
0,
this.maskWidth,
this.maskHeight
);
let data = this.setCanvasColor({
controlImgData,
selectImgData,
pickUpColor: this.pickColor,
rgb: [48, 57, 218, 150],
isClcik,
});
mouseContext.putImageData(data.controlImgData, 0, 0);
if (data.selectImgData)
clickContext.putImageData(data.selectImgData, 0, 0);
},
//设置图层颜色
setCanvasColor({
controlImgData, //操作图层
selectImgData, //选择图层
pickUpColor, //选中区域色值
rgb, //替换颜色
isClcik,
}) {
let area = colorMap.get(pickUpColor.join(","));
if (area) {
if (isClcik) {
maskArea.push(area);
selectColor.push(pickUpColor.join(","));
}
area.map((pixel) => {
let i = pixel * 4;
if (isClcik) {
selectImgData.data[i] = rgb[0];
selectImgData.data[i + 1] = rgb[1];
selectImgData.data[i + 2] = rgb[2];
selectImgData.data[i + 3] = rgb[3];
} else {
controlImgData.data[i] = rgb[0];
controlImgData.data[i + 1] = rgb[1];
controlImgData.data[i + 2] = rgb[2];
controlImgData.data[i + 3] = rgb[3];
}
});
}
return {
controlImgData,
selectImgData,
};
}
保存蒙板
保存蒙板相对简单,由于AI只能识别黑白色值,我们需讲选区图层选中部分替换成黑色,未选中部分替换成白色。由于我事先记录了选区所涉及到了所有colorMap项,所以只需初始化一张纯白的底图,遍历绘制黑色选区即可。核心代码如下~
//选定区域画布像素颜色值
const maskData = maskContext.getImageData(
0,
0,
this.maskWidth,
this.maskHeight
);
let black = [0, 0, 0, 255];
maskArea.map((area) => {
area.map((pixel) => {
let i = pixel * 4;
maskData.data[i] = black[0];
maskData.data[i + 1] = black[1];
maskData.data[i + 2] = black[2];
maskData.data[i + 3] = black[3];
});
});
maskContext.putImageData(maskData, 0, 0);
结语
本篇文章主要对 AI 换图中局部编辑的一种实现方案剖析,有不明白的欢迎留言👏,创作不易留下你的点赞~