AI:局部编辑的无限可能

998 阅读3分钟

揭示局部奇观

前言

本文探索热门AI换图技术,深入探讨图像生成、局部替换与保留的解决方案。

图生图原理

AI 生成图的流程示意

graph LR
 主图 --> 色域图 --> 蒙板图-->  局部编辑--> 蒙板图 -->  风格 -->  咒语  --> AI生成

局部编辑原理

这里只关心前端的实现,本文重点介绍局部编辑的方案。下方是整体局部编辑图层信息层级递增及数据层信息。

主图图层:展示主图图片
选区蒙板图层:绘制鼠标选中区域遮罩图
鼠标操作图层:鼠标移入移除显示遮罩区域图
色域图数据:基础色域划分图,提供遮罩图的生成规则
journey
section 可视区
主图图层: 6: 层级 1
选区蒙板图层: 4: 层级 2
鼠标操作图层: 2: 层级 3
section 数据层
色域图数据: 5: 数据层
选中色域 Map: 5: 数据层

效果图

Sep-22-2023 10-09-01.gif

实现步骤

graph LR
 色域图 --> getImageData --> 切片 -->  webworker -->  合并Map -->  colorMap -->  色域图--> 选区透明图层 -->  鼠标透明图层 -->  获取鼠标位置 --> 拾取位置颜色--> 颜色获取对应Map像素集合  -->  设置选区图层颜色 -->  选区透明图层  

色域图生成 colorMap

Snipaste_2023-09-22_10-29-45.png

左侧为色域图,我们拿到色域图需要将其用 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 换图中局部编辑的一种实现方案剖析,有不明白的欢迎留言👏,创作不易留下你的点赞~