HarmonyOS鸿蒙Next中代码工坊的AI matting示例代码抠图,想要保存带白边的效果如何实现

HarmonyOS鸿蒙Next中代码工坊的AI matting示例代码抠图,想要保存带白边的效果如何实现 【问题描述】:参照代码工坊的AI matting示例代码抠图,图片选中之后,周围有一圈白边效果,但是复制之后粘贴就没有白边的效果,我想把抠出来的图片做成贴纸,这个白边效果正好是我想要的,请问如何能保存这个白边的效果,或者是如何给一个图片添加这个白边的效果,有方案实现吗,效果图如下

【问题现象】:

抠图效果

cke_14752.png

粘贴后的效果

cke_18794.png

【版本信息】:API20

【复现代码】:不涉及

【尝试解决方案】:不涉及


更多关于HarmonyOS鸿蒙Next中代码工坊的AI matting示例代码抠图,想要保存带白边的效果如何实现的实战教程也可以访问 https://www.itying.com/category-93-b0.html

7 回复

要实现 “保存 AI 抠图后的白边 / 发光边缘效果”,核心是在抠图得到前景PixelMap后,手动通过 Canvas 绘制边缘效果,再生成新的带边缘的PixelMap并保存(示例代码的预览白边是临时绘制的,未随原图复制,需主动添加边缘后再保存)。以下是 API20 下的具体实现方案:

原理说明

AI Matting 示例的 “白边效果” 是预览时通过 Canvas 临时绘制的边缘,但复制时仅导出了纯前景PixelMap。因此需要:

  1. 拿到抠图后的前景PixelMap
  2. 创建新的PixelMap(尺寸比前景大,预留边缘空间);
  3. 用 Canvas 在新PixelMap上先画 “边缘效果”,再叠加原前景;
  4. 保存最终的带边缘PixelMap为图片文件。

步骤 1:获取抠图后的前景PixelMap

先通过 AI Matting 接口得到抠图后的前景PixelMap(假设示例代码中已得到foregroundPixelMap)。

步骤 2:绘制边缘效果并生成新PixelMap

通过Canvas绘制边缘(以 “发光白边” 为例,也可改为纯色白边),并生成带边缘的新PixelMap

import image from '@ohos.multimedia.image';
import common from '@ohos.app.ability.common';
import fs from '@ohos.file.fs';

// 假设已得到抠图后的前景PixelMap
async function addEdgeAndSave(foregroundPixelMap: image.PixelMap, context: common.UIAbilityContext) {
  // 1. 获取前景图片信息
  const imgInfo = foregroundPixelMap.getImageInfo();
  const edgeWidth = 10; // 边缘宽度(可自定义)
  const newWidth = imgInfo.width + 2 * edgeWidth; // 新图宽度(前景+两边边缘)
  const newHeight = imgInfo.height + 2 * edgeWidth; // 新图高度

  // 2. 创建新的PixelMap(用于绘制带边缘的图像)
  const newPixelMap = await image.createPixelMap({
    width: newWidth,
    height: newHeight,
    pixelFormat: image.PixelFormat.ARGB_8888, // 支持透明通道
    alphaType: image.AlphaType.PREMUL_ALPHA,
    colorSpace: image.ColorSpace.SRGB
  });

  // 3. 绑定Canvas到新PixelMap,绘制边缘+前景
  const canvas = new Canvas();
  const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
  await ctx.bindPixelMap(newPixelMap); // 关联Canvas与新PixelMap

  // 移动画布原点到边缘位置(方便后续绘制)
  ctx.translate(edgeWidth, edgeWidth);

  // 4. 绘制“发光白边”效果(先画模糊的白色轮廓,再画前景)
  ctx.save();
  // 放大前景尺寸,模拟边缘范围
  ctx.scale(1 + 2 * edgeWidth / imgInfo.width, 1 + 2 * edgeWidth / imgInfo.height);
  // 设置模糊效果(API20支持基础filter)
  ctx.filter = 'blur(3px)';
  // 绘制半透明白色前景(作为发光边缘)
  ctx.globalAlpha = 0.8;
  ctx.drawImage(foregroundPixelMap, 0, 0, imgInfo.width, imgInfo.height);
  ctx.restore();

  // 5. 绘制原前景到中间(覆盖边缘,保留主体清晰)
  ctx.globalAlpha = 1;
  ctx.drawImage(foregroundPixelMap, 0, 0, imgInfo.width, imgInfo.height);

  // 6. 解除Canvas与PixelMap的绑定
  await ctx.unbindPixelMap();

  // 7. 保存带边缘的PixelMap为图片文件(存入应用沙箱)
  const imagePacker = image.createImagePacker();
  const saveOption = {
    format: 'png', // 保存为PNG(支持透明)
    quality: 100
  };
  const packedData = await imagePacker.packing(newPixelMap, saveOption); // 打包为图片二进制数据

  // 写入沙箱文件(示例路径:应用沙箱的cache目录)
  const cacheDir = await context.getCacheDir();
  const savePath = `${cacheDir}/sticker_with_edge.png`;
  const file = await fs.open(savePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  await fs.write(file.fd, packedData);
  await fs.close(file.fd);

  console.log(`带边缘的贴纸已保存到:${savePath}`);
  return savePath; // 返回保存路径
}

步骤 3:调用方法(在 UIAbility 中使用)

在你的页面 / 组件中调用上述方法(需传入foregroundPixelMap和 UIAbility 上下文):

// 假设在组件的onClick事件中触发
async handleSaveSticker() {
  const context = getContext(this) as common.UIAbilityContext;
  // (这里替换为你从AI Matting得到的foregroundPixelMap)
  const foregroundPixelMap = await this.getMattingResult(); 
  // 执行添加边缘并保存
  const savePath = await addEdgeAndSave(foregroundPixelMap, context);
  // 后续可通过savePath访问带边缘的贴纸文件
}

效果调整(可选)

  • 若需要纯色白边:替换步骤 4 的 “发光效果” 为纯色矩形绘制:
    // 绘制纯色白边(替代步骤4)
    ctx.fillStyle = '#ffffff'; // 白边颜色
    ctx.fillRect(-edgeWidth, -edgeWidth, imgInfo.width + 2 * edgeWidth, imgInfo.height + 2 * edgeWidth);
    
  • 调整边缘宽度 / 颜色:修改edgeWidthctx.fillStylectx.globalAlpha等参数即可。

核心说明

通过 Canvas 手动绘制边缘后,新生成的PixelMap包含了 “边缘 + 前景”,保存后就是你需要的贴纸效果。此方案适配 API20,利用鸿蒙原生imageCanvas接口实现,无需依赖第三方库。

更多关于HarmonyOS鸿蒙Next中代码工坊的AI matting示例代码抠图,想要保存带白边的效果如何实现的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


我用了另外一种方案来实现楼主可以参考,更改为图片加白边

【解决方案】

读取像素:获取扣好图的 PixelMap 数据。

生成白底:遍历所有不透明的像素,在它的周围(由边框宽度决定)填充白色。这就像把主体“放大”了一圈白色。

合成:将原始的主体图片覆盖在这个“白色放大版”上面。

输出:生成一张新的带白边的图片。

【实现效果】

cke_4586.jpeg

【demo代码】

import image from '@ohos.multimedia.image';
import promptAction from '@ohos.promptAction';

@Entry
@Component
struct StickerPage {
  @State originalImage: image.PixelMap | null = null;
  @State stickerImage: image.PixelMap | null = null;
  @State isProcessing: boolean = false;
  @State strokeWidth: number = 10; // 默认描边宽度

  // 页面加载时生成一个测试用的透明底图片
  async aboutToAppear() {
    this.originalImage = await this.createMockPixelMap();
  }

  // 创建一个测试图片(透明背景上的红色爱心),模拟抠图结果
  async createMockPixelMap(): Promise<image.PixelMap> {
    const width = 300;
    const height = 300;
    const buffer = new ArrayBuffer(width * height * 4);
    const pixels = new Uint8Array(buffer); // RGBA

    // 简单的算法画一个爱心或者圆形,这里画一个红色的圆作为主体
    const cx = width / 2;
    const cy = height / 2;
    const radius = 80;

    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        const index = (y * width + x) * 4;

        // 简单的圆形方程
        const dist = Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy));

        if (dist < radius) {
          // 红色主体 (R=255, G=0, B=0, A=255)
          pixels[index] = 255;
          pixels[index + 1] = 0;
          pixels[index + 2] = 0;
          pixels[index + 3] = 255;
        } else {
          // 透明背景
          pixels[index] = 0;
          pixels[index + 1] = 0;
          pixels[index + 2] = 0;
          pixels[index + 3] = 0;
        }
      }
    }

    const opts: image.InitializationOptions = {
      editable: true,
      pixelFormat: image.PixelMapFormat.RGBA_8888,
      size: { height, width }
    };
    return await image.createPixelMap(buffer, opts);
  }

  // 生成贴纸的核心方法
  async generateSticker() {
    if (!this.originalImage) {
      return;
    }

    // 防止重复点击
    if (this.isProcessing) {
      return;
    }

    this.isProcessing = true;
    const startTime = Date.now();

    try {
      // 调用工具类
      // 这里的 strokeWidth 是界面上滑块控制的
      const result = await StickerUtils.addStickerStroke(this.originalImage, this.strokeWidth);

      this.stickerImage = result;

      const duration = Date.now() - startTime;
      promptAction.showToast({ message: `生成完毕,耗时: ${duration}ms` });

    } catch (error) {
      console.error('生成失败:', error);
      promptAction.showToast({ message: '生成失败,请查看日志' });
    } finally {
      this.isProcessing = false;
    }
  }

  build() {
    Column() {
      // 标题栏
      Text('贴纸白边效果演示')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .width('100%')
        .padding(15)
        .backgroundColor('#F1F3F5')
        .textAlign(TextAlign.Center)

      Scroll() {
        Column({ space: 20 }) {

          // 1. 展示原图区域
          Column() {
            Text('1. 原始抠图 (透明背景)')
              .fontSize(14)
              .fontColor(Color.Gray)
              .margin({ bottom: 8 })

            Stack() {
              // 棋盘格背景,用于展示透明效果
              GridRow({ columns: 20 }) {
                ForEach(new Array(400).fill(0), (_: number, index: number) => {
                  GridCol()
                    .height(15)
                    .backgroundColor(index % 2 === 0 ? '#E0E0E0' : '#FFFFFF')
                })
              }
              .width(300)
              .height(300)
              .clip(true)

              if (this.originalImage) {
                Image(this.originalImage)
                  .width(300)
                  .height(300)
                  .objectFit(ImageFit.Contain)
              }
            }
            .borderRadius(8)
            .clip(true)
            .border({ width: 1, color: '#CCCCCC' })
          }

          // 2. 控制区域
          Column() {
            Row() {
              Text(`白边宽度: ${this.strokeWidth.toFixed(0)}px`)
                .fontSize(16)
                .fontWeight(FontWeight.Medium)
            }
            .width('100%')
            .justifyContent(FlexAlign.Start)

            Slider({
              value: this.strokeWidth,
              min: 1,
              max: 30,
              step: 1,
              style: SliderStyle.OutSet
            })
              .showSteps(true)
              .onChange((value: number) => {
                this.strokeWidth = value;
              })
          }
          .padding({ left: 20, right: 20 })
          .width('100%')

          Button(this.isProcessing ? '处理中...' : '生成白边贴纸')
            .width('80%')
            .height(45)
            .backgroundColor(this.isProcessing ? '#CCCCCC' : '#007DFF')
            .onClick(() => {
              this.generateSticker();
            })

          // 3. 展示结果区域
          if (this.stickerImage) {
            Column() {
              Text('2. 结果预览 (深色背景)')
                .fontSize(14)
                .fontColor(Color.Gray)
                .margin({ bottom: 8 })

              Stack() {
                // 深色背景,方便看清白边
                Rect()
                  .width('100%')
                  .height('100%')
                  .fill('#333333')

                Image(this.stickerImage)
                  .width(300)
                  .height(300)
                  .objectFit(ImageFit.Contain)
              }
              .width(300)
              .height(300)
              .borderRadius(8)
              .clip(true)
            }
            .animation({ duration: 300 })
          }
        }
        .padding({ top: 20, bottom: 50 })
        .width('100%')
      }
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFFFFF')
  }
}


export class StickerUtils {
  /**
   * 给图片添加白色描边效果(制作贴纸)
   * @param sourcePixelMap 原始图片 (必须是背景透明的)
   * @param strokeWidth 描边宽度 (像素单位,建议 5-20)
   * @returns 处理后的 PixelMap
   */
  public static async addStickerStroke(sourcePixelMap: image.PixelMap,
    strokeWidth: number = 10): Promise<image.PixelMap> {
    const imageInfo = await sourcePixelMap.getImageInfo();
    const width = imageInfo.size.width;
    const height = imageInfo.size.height;

    // 1. 读取原始像素数据
    const bufferData = new ArrayBuffer(sourcePixelMap.getPixelBytesNumber());
    await sourcePixelMap.readPixelsToBuffer(bufferData);

    // 使用 Uint8Array 操作字节,或者是 Int32Array 操作像素点 (RGBA)
    // 这里使用 Uint32Array 方便处理整个像素颜色,但在 ArkTS 中需注意大小端问题
    // 为兼容性,我们操作 Uint8Array: [R, G, B, A, R, G, B, A...]
    const srcPixels = new Uint8Array(bufferData);

    // 2. 创建一个新的 Buffer 用于存放结果
    // 结果图大小不变(如果主体太大贴边,描边可能会被切掉,建议输入图主体周围留有余白)
    const dstPixels = new Uint8Array(bufferData.byteLength);

    // 预设白色 (R=255, G=255, B=255, A=255)
    // 注意:PixelMap 默认格式通常是 RGBA_8888

    // 3. 算法核心:膨胀 (Dilation) 生成白底
    // 这是一个高强度的计算,为了性能,我们通过判断“当前点是否不透明”来向四周扩散

    // 优化:我们不需要遍历每一个点去画圆,而是遍历原图,如果原图某点有像素,
    // 就在目标图对应位置及周围画白。

    // 为了防止计算量过大卡顿,这里使用简单的“方框+圆形”逻辑
    const w = width;
    const h = height;
    const dist = Math.ceil(strokeWidth);
    const distSq = dist * dist;

    // 填充 0 (全透明)
    dstPixels.fill(0);

    for (let y = 0; y < h; y++) {
      for (let x = 0; x < w; x++) {
        const idx = (y * w + x) * 4;
        const alpha = srcPixels[idx + 3]; // 获取 Alpha 通道

        // 如果当前像素不是透明的 (阈值设为 10 过滤杂边)
        if (alpha > 10) {
          // 在目标图上,以 (x,y) 为中心,dist 为半径画白色
          // 优化循环范围,只遍历由于描边宽度影响到的区域
          const startY = Math.max(0, y - dist);
          const endY = Math.min(h - 1, y + dist);
          const startX = Math.max(0, x - dist);
          const endX = Math.min(w - 1, x + dist);

          for (let dy = startY; dy <= endY; dy++) {
            for (let dx = startX; dx <= endX; dx++) {
              // 检查距离是否在圆内 (制作圆角描边)
              const deltaX = dx - x;
              const deltaY = dy - y;
              if (deltaX * deltaX + deltaY * deltaY <= distSq) {
                const dstIdx = (dy * w + dx) * 4;
                // 设置为白色不透明
                dstPixels[dstIdx] = 255; // R
                dstPixels[dstIdx + 1] = 255; // G
                dstPixels[dstIdx + 2] = 255; // B
                dstPixels[dstIdx + 3] = 255; // A
              }
            }
          }
        }
      }
    }

    // 4. 将原始图片覆盖在白底之上
    for (let i = 0; i < srcPixels.length; i += 4) {
      const srcA = srcPixels[i + 3];
      if (srcA > 0) {
        // 简单的 Alpha 混合:如果原图有像素,直接覆盖
        // (更严谨的做法是做 alpha blending,但对于贴纸,直接覆盖通常效果最好)
        dstPixels[i] = srcPixels[i]; // R
        dstPixels[i + 1] = srcPixels[i + 1]; // G
        dstPixels[i + 2] = srcPixels[i + 2]; // B
        dstPixels[i + 3] = srcPixels[i + 3]; // A
      }
    }

    // 5. 创建新的 PixelMap
    const opts: image.InitializationOptions = {
      editable: true,
      pixelFormat: image.PixelMapFormat.RGBA_8888,
      size: { height: height, width: width }
    };

    const newPixelMap = await image.createPixelMap(dstPixels.buffer, opts);
    return newPixelMap;
  }
}

可以参考:拖拽图像增加水印

改成拖拽后加个白边就好了。

期待HarmonyOS能在未来带来更多创新的技术和理念。

这种异形的图片怎么增加白边

在HarmonyOS Next的AI Matting示例中,要保存带白边的抠图效果,需在ImageMatting处理完成后,对生成的Alpha遮罩进行边缘扩展。具体操作是:将遮罩二值化后,使用dilate形态学操作扩展边缘,再与原始图像合成。代码中需在ImageMattingonResult回调里,对返回的PixelMap(遮罩)进行边缘处理,然后通过ImagePacker将处理后的遮罩与原图结合,生成带白边的最终图像并保存。

在HarmonyOS Next中,AI Matting示例代码生成的抠图结果(Alpha遮罩)默认是透明背景。您看到的“白边”效果实际上是UI组件(如Image)在透明区域叠加的视觉边框,并非图像数据的一部分。要实现保存带白边的贴纸效果,需要对抠图结果进行后处理。

核心方案是:获取抠图结果的Alpha通道,创建一个新图像,在透明区域(Alpha值较低的区域)填充白色,然后将处理后的图像保存到文件

以下是基于@kit.ArkGraphics2D(2D图形库)的关键实现步骤:

  1. 获取图像数据:从AI Matting的输出结果(通常是一个包含RGBA数据的PixelMap对象)中获取原始的像素数据。
  2. 处理Alpha通道:遍历每个像素的Alpha值。您可以设定一个阈值(例如,Alpha < 10)。对于低于此阈值的像素(即原图中完全透明或接近透明的区域),将其RGB通道的值设置为白色(255, 255, 255),同时将Alpha通道设置为不透明(255)。对于Alpha值较高的像素(主体部分),保留其原有的RGB颜色和Alpha值。
  3. 创建并保存新图像:将处理后的像素数据重新封装为PixelMap,然后使用image组件的packing方法或相关API将其编码为PNG等格式保存到应用沙箱路径。

示例代码片段

import { image } from '@kit.ArkGraphics2D';
import { BusinessError } from '@kit.BasicServicesKit';

// 假设 mattingResultPixelMap 是AI Matting后得到的 PixelMap 对象
async function addWhiteBorderAndSave(mattingResultPixelMap: image.PixelMap): Promise<void> {
  const imageInfo: image.InitializationOptions = mattingResultPixelMap.getImageInfo();
  const buffer: ArrayBuffer = await mattingResultPixelMap.getPixelBytesBuffer();

  // 获取像素数据(假设为RGBA_8888格式)
  const pixelArray = new Uint8Array(buffer);
  const alphaThreshold = 10; // 透明度阈值,可调整

  for (let i = 0; i < pixelArray.length; i += 4) {
    const alpha = pixelArray[i + 3];
    if (alpha < alphaThreshold) {
      // 透明区域填充为白色不透明
      pixelArray[i] = 255;     // R
      pixelArray[i + 1] = 255; // G
      pixelArray[i + 2] = 255; // B
      pixelArray[i + 3] = 255; // A
    }
    // 非透明区域保留原色
  }

  // 创建新的PixelMap
  const newPixelMap: image.PixelMap = await image.createPixelMap(pixelArray.buffer, imageInfo);

  // 保存为PNG文件到应用沙箱
  const packOpts: image.PackingOption = {
    format: "image/png",
    quality: 100
  };
  const filePath = '您的沙箱路径/贴纸_with_border.png'; // 替换为实际路径
  await image.packing(newPixelMap, filePath, packOpts);

  // 释放资源
  newPixelMap.release();
}

注意事项

  • 确保已申请必要的文件读写权限(ohos.permission.READ_MEDIA, ohos.permission.WRITE_MEDIA)。
  • 处理完成后及时调用release()释放PixelMap资源,避免内存泄漏。
  • 阈值alphaThreshold可根据实际视觉效果调整,以控制白边的“硬度”。

此方法直接修改图像数据,保存后的文件将永久包含白色边框,适合用于创建贴纸等素材。

回到顶部