微信小程序 + 腾讯云人体分析:从 0 到 1 实现 AI 抠图打卡合照(细节待更新~)

前言:

前段时间给客户做了一款线下活动小程序,里面有个需求:用户在现场拍一张人像,系统自动把人抠出来,合成到活动背景上,生成一张带 AI 标识的打卡合照。说白了就是一个"AI 打卡合照"功能。

我调研了几套方案:

  1. 非主流方法:纯前端的方式找开源抠图模型,复制或安装到项目之中进行使用,最后就是折腾半天没成功过...
  2. 主流办法 :Ai抠图模型的使用(腾讯云人体分析 BDA) SegmentPortraitPic 接口专门做人像分割,按量计费(每月有赠送1000的请求抠图次数),返回支持 url,和因为小程序用的就是云服务所以搭配腾讯人体分析有互通使用起来还是不错,

一、最终效果与整体流程

页面操作流程:

复制代码
选择活动背景 → 拍照 / 从相册选人像 → 等待 AI 抠图 → 拖拽缩放旋转人像 → 保存打卡合照

技术层面:

复制代码
小程序端拍照/选图
    ↓
智能压缩(超过 3.5MB 压到 1920px)
    ↓
上传图片到小程序云存储
    ↓
调用云函数 segment-portrait
    ↓
云函数下载图片 → 调腾讯云 SegmentPortraitPic → 拿到抠图结果 url
    ↓
下载抠图结果 → 上传回云存储 → 返回 fileID
    ↓
小程序端获取临时 url → Canvas 合成背景+人像
    ↓
嵌入 AI 生成水印 → 保存到相册

为什么要把 AI 接口放在云函数里?

  1. SecretId / SecretKey 不能暴露在前端。
  2. 腾讯云 API 需要 TC3-HMAC-SHA256 签名,云函数里用 Node.js crypto 原生成,零依赖。
  3. 大图片 base64 传输容易超限,云函数里走 url 输出类型更稳。(其实最开始我就是本地项目,直接请求抠图ai模型的导致传输内容太大抠图失败,除非使用很1MB内的图片,否则很容易抠图失败)

二、腾讯云:开通人体分析并拿到密钥【开通子账户,并设置对应抠图权限】
  1. 登录 腾讯云控制台
  2. 搜索并开通 人体分析(BDA) 服务。
  3. 进入 API 密钥管理,新建密钥,拿到 SecretIdSecretKey
  4. 把密钥配置到云函数里,不要写死在前端
javascript 复制代码
// cloudfunctions/segment-portrait/index.js
const SECRET_ID  = '你的 SecretId';
const SECRET_KEY = '你的 SecretKey';
const REGION  = 'ap-shanghai';
const SERVICE = 'bda';
const HOST    = 'bda.tencentcloudapi.com';
const ACTION  = 'SegmentPortraitPic';
const VERSION = '2020-03-24';

云函数 config.json 里记得把内存提到 512MB,大图片很容易把 128MB/256MB 打爆,导致 OOM。


三、小程序端:拍照、压缩、上传云存储

用户拍照或者选图之后,别急着传,先判断大小。腾讯云接口图片上限 5MB,前端走 base64 到云函数还会膨胀约 1.33 倍,所以我把安全阈值定在 3.5MB

javascript 复制代码
processAndCompose: function (photoPath) {
  var that = this;
  this.setData({ processing: true, processingText: '正在准备...' });

  var fs = wx.getFileSystemManager();
  fs.stat({
    path: photoPath,
    success: function (stat) {
      var size = stat && stat.size ? stat.size : 0;
      if (size > 0 && size <= 3.5 * 1024 * 1024) {
        that._uploadAndSegment(photoPath);
      } else {
        wx.compressImage({
          src: photoPath,
          quality: 92,
          compressedWidth: 1920,
          success: function (res) {
            that._uploadAndSegment(res.tempFilePath);
          },
          fail: function () {
            that._uploadAndSegment(photoPath);
          }
        });
      }
    }
  });
}

压缩完上传到云存储:

javascript 复制代码
_uploadAndSegment: function (photoPath) {
  var that = this;
  var cloudPath = 'portrait-origin/' + Date.now() + '_origin.jpg';

  wx.cloud.uploadFile({ cloudPath: cloudPath, filePath: photoPath })
    .then(function (uploadRes) {
      return wx.cloud.callFunction({
        name: 'segment-portrait',
        data: { personFileID: uploadRes.fileID }
      });
    })
    .then(function (fnRes) {
      var result = fnRes.result || {};
      if (!result.success) {
        that.showError(result.message || 'AI 抠图失败');
        return;
      }
      if (!result.hasPerson) {
        that.showError('未检测到清晰人像,请重新拍照');
        return;
      }
      return wx.cloud.getTempFileURL({ fileList: [result.resultFileID] });
    })
    .then(function (urlRes) {
      if (urlRes && urlRes.fileList && urlRes.fileList[0]) {
        that.renderComposeCanvas(urlRes.fileList[0].tempFileURL);
      }
    })
    .catch(function (err) {
      that.showError(err.message || '网络异常');
    });
}

注意 :用户取消相册选择时 errMsg 里会带 cancel,这种不要弹错误框,静默返回即可,否则体验很差。


四、云函数核心:TC3-HMAC-SHA256 签名 + 人像分割

腾讯云 API 3.0 要求每个请求都带签名。如果你在云函数里不想额外引入 SDK,可以直接用 Node.js 原生 crypto 实现 TC3-HMAC-SHA256

javascript 复制代码
function sha256(data, secret) {
  return secret
    ? crypto.createHmac('sha256', secret).update(data).digest()
    : crypto.createHash('sha256').update(data).digest();
}

function buildAuthHeaders(params) {
  const timestamp = Math.floor(Date.now() / 1000);
  const date = new Date(timestamp * 1000).toISOString().substring(0, 10);
  const payload = JSON.stringify(params);

  const canonicalHeaders = 'content-type:application/json; charset=utf-8\nhost:' + HOST + '\n';
  const signedHeaders = 'content-type;host';
  const hashedRequestPayload = sha256(payload).toString('hex');
  const canonicalRequest = [
    'POST', '/', '', canonicalHeaders, signedHeaders, hashedRequestPayload
  ].join('\n');

  const credentialScope = date + '/' + SERVICE + '/tc3_request';
  const stringToSign = [
    'TC3-HMAC-SHA256', timestamp, credentialScope,
    sha256(canonicalRequest).toString('hex')
  ].join('\n');

  const kDate = sha256(date, 'TC3' + SECRET_KEY);
  const kService = sha256(SERVICE, kDate);
  const kSigning = sha256('tc3_request', kService);
  const signature = sha256(stringToSign, kSigning).toString('hex');

  const authorization = 'TC3-HMAC-SHA256 ' +
    'Credential=' + SECRET_ID + '/' + credentialScope + ', ' +
    'SignedHeaders=' + signedHeaders + ', ' +
    'Signature=' + signature;

  return {
    'Authorization': authorization,
    'Content-Type': 'application/json; charset=utf-8',
    'Host': HOST,
    'X-TC-Action': ACTION,
    'X-TC-Version': VERSION,
    'X-TC-Timestamp': timestamp,
    'X-TC-Region': REGION,
  };
}

调接口时,一定用 RspImgType: 'url',不要 base64。base64 在大图场景下很容易触发云函数返回体大小限制,而且小程序端接收也慢。

javascript 复制代码
var bdaResult = await callBdaAPI({
  Image: imageBase64,
  RspImgType: 'url', // 输出用 URL,避免 base64 超限
});

拿到 ResultImageUrl 之后,云函数再下载下来,上传回小程序云存储,最后返回 fileID 给前端。为什么多这一步?因为腾讯云返回的 url 是临时 url,有效期很短,前端直接用它画 canvas 可能画到一半就失效了,落到云存储里最稳~


五、云函数重试:限流、超时、偶发失败都能扛(这一步能解决代码层面的抠图成功率达到80%以上,当然截至目前还未发现抠图失败情况加上以后,之前因为网络或文件体积或云函数超时等问题,抠图成功率全靠手气)

人像分割是 AI 接口,偶发限流或内部错误很正常。我在云函数里加了 3 次重试:

javascript 复制代码
var retryableErrors = [
  'RequestLimitExceeded',
  'InternalError',
  'FailedOperation.RequestTimeout',
  'FailedOperation.ServerError',
  'FailedOperation.RpcFail',
  'FailedOperation.SegmentFailed'
];

if (retryableErrors.indexOf(errCode) !== -1 && retryCount < maxRetries) {
  var delay = (retryCount + 1) * 1000;
  setTimeout(function () {
    resolve(callBdaAPI(params, retryCount + 1));
  }, delay);
  return;
}

建议 maxRetries = 3,每次延迟递增,既不会太激进,也不会让用户等太久。


六、Canvas 合成:背景保持原尺寸,人像可拖拽缩放旋转

画布尺寸我按背景图原始尺寸来初始化,这样保存出来的图片不会变形,也不会被强制裁切。

javascript 复制代码
initCanvasSize: function (bgW, bgH) {
  var systemInfo = wx.getSystemInfoSync();
  var sw = systemInfo.windowWidth;
  var sh = systemInfo.windowHeight - 100;
  var maxDisplayH = sh * 0.78;

  var scale = Math.min(sw / bgW, maxDisplayH / bgH);
  this.setData({
    canvasStyleWidth: bgW * scale,
    canvasStyleHeight: bgH * scale,
    canvasWidth: bgW,
    canvasHeight: bgH
  });
  this._displayScale = scale;
}

canvas 节点用 type="2d",绘制时先画背景,再画人像。人像支持单指拖拽、双指缩放旋转:

javascript 复制代码
onTouchStart: function (e) {
  var t = e.touches;
  if (t.length === 1) {
    this._isDragging = true;
    this._touchStartX = this._toCanvasX(t[0].x);
    this._touchStartY = this._toCanvasY(t[0].y);
  } else if (t.length === 2) {
    this._initialDistance = this._dist(t[0], t[1]);
    this._initialScale = this._personScale;
    this._initialRotation = this._personRotation;
    this._initialAngle = Math.atan2(t[1].y - t[0].y, t[1].x - t[0].x);
  }
}

onTouchMove: function (e) {
  var t = e.touches;
  if (t.length === 1 && this._isDragging) {
    var canvasX = this._toCanvasX(t[0].x);
    var canvasY = this._toCanvasY(t[0].y);
    this._personX += canvasX - this._touchStartX;
    this._personY += canvasY - this._touchStartY;
    this._touchStartX = canvasX;
    this._touchStartY = canvasY;
  } else if (t.length === 2) {
    var d = this._dist(t[0], t[1]);
    this._personScale = Math.max(0.1, Math.min(3,
      this._initialScale * (d / this._initialDistance)));
    var a = Math.atan2(t[1].y - t[0].y, t[1].x - t[0].x);
    this._personRotation = this._initialRotation + (a - this._initialAngle);
  }
  this._throttle();
}

绘制时以人像中心点为基准做变换:

javascript 复制代码
var cx = this._personX + (img.width * this._personScale) / 2;
var cy = this._personY + (img.height * this._personScale) / 2;
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(this._personRotation);
ctx.scale(this._personScale, this._personScale);
ctx.drawImage(img, -img.width / 2, -img.height / 2, img.width, img.height);
ctx.restore();

七、AI 生成标识:水印必须画进图片里

这是审核重点。只是页面右上角飘一个"AI 生成"标签,微信审核会直接打回,因为保存到相册的图片上没有标识。

正确做法:在 canvas 绘制阶段就把水印写进去,同时页面保留一个视觉提示作为二次提醒。

javascript 复制代码
// ===== AI 生成显著标识(画入图片,审核要求) =====
ctx.save();
var fontSize = Math.max(24, Math.floor(w * 0.03));
ctx.font = 'bold ' + fontSize + 'px sans-serif';
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.textAlign = 'right';
ctx.textBaseline = 'bottom';
ctx.shadowColor = 'rgba(0,0,0,0.45)';
ctx.shadowBlur = 8;
ctx.shadowOffsetX = 1;
ctx.shadowOffsetY = 1;
ctx.fillText('AI 生成 · 人工智能合成', w - 28, h - 28);
ctx.restore();

字号取画布宽度的 3%,同时限制最小 24px,保证不同尺寸背景图都能看清。


八、保存图片与错误处理

合成完成后,用 canvasToTempFilePath 导出图片,再调用 saveImageToPhotosAlbum 保存到相册。

错误弹框我统一封装了一个组件,要求:

  • 必须能显示原始错误日志(最多 3 行,超出省略)。
  • 必须显示原因分析
  • 必须有"我知道了"按钮,且不能点蒙层关闭
  • 右上角保留关闭按钮。
javascript 复制代码
showError: function (log, key) {
  var info = errorHelper.getErrorInfo(key || log);
  this.setData({
    processing: false,
    errorDialog: {
      show: true,
      title: info.title,
      log: log || '无日志',
      cause: info.cause
    }
  });
}

保存失败时也不要用 toast 一闪而过,必须用这个自定义错误框把原因讲清楚。


九、踩坑总结
  1. 不要从前端直接调腾讯云 API:密钥会泄露,而且前端签名校验环境不稳定。
  2. AI 接口返回用 url,不要用 base64:base64 大图直接打爆云函数返回限制。
  3. 云函数内存开到 512MB:处理高清图片时 128MB/256MB 很容易 OOM。
  4. 背景图不要打进分包:高清背景图体积大,建议放云存储或 CDN,按 url 下载。
  5. 水印必须嵌入合成图:只做页面 overlay 审核过不了。
  6. 用户取消选图不算错误errMsgcancel 时静默处理。
  7. 抠图结果 url 有效期短:云函数下载后再传回云存储,前端用云存储临时 url 更稳。
  8. 分包大小控制在 2MB 以内:如果背景图很多,考虑按需下载,不要预置在包里。

十、结语

整个方案走下来,核心就是三段:前端负责选图压缩和交互、云函数负责安全签名和 AI 调用、Canvas 负责合成输出 。腾讯云 SegmentPortraitPic 的抠图效果对于打卡合照这种场景完全够用,头发丝级别的精细度可能比不上专业修图软件,但活动现场快速生成一张可用合照绰绰有余。

如果你也在做类似功能,建议先把云函数签名调通,再拼前端 Canvas,最后处理水印和审核细节。祝大家一次过审。


标签:

#微信小程序 #腾讯云 #人体分析 #AI抠图 #小程序云开发

相关推荐
xingyuzhisuan1 小时前
团队实践:引入 Vera 1.1 之后,视频内容团队工作流重构
人工智能·重构·音视频
晴天161 小时前
Cordis 框架代码核心解析:一个可逆插件系统的实现-Day18
人工智能·ai·架构
跨境卫士苏苏1 小时前
2026年做跨境电商,TikTok美区半托管这3个品类正在严查,第2个很多新手还在铺货
大数据·人工智能·跨境电商·营销策略
魈十三1 小时前
2026在线会议软件推荐:8款工具对比评测与多人协作选型指南
人工智能
GIS数据转换器1 小时前
村镇无人机物流配送与跨域监测一体化平台
大数据·运维·人工智能·科技·无人机
努力进修1 小时前
“数据心脏” 驱动能源自主:国产数据库支撑 Cemsol 实现固井全流程数字化闭环
数据库·人工智能·能源
心运软件1 小时前
基于深度学习的宝石图像分类系统
人工智能·python·深度学习·机器学习·分类·数据挖掘
雨晨源码(同名B站)1 小时前
【2027届人工智能专业选题】基于yolov8的农业病虫害图像识别与分类系统 |深度学习 计算机视觉
人工智能·深度学习·yolo·计算机视觉·分类
格林威1 小时前
多相机并行采图最佳实践:Task.WhenAll + 异常处理 + 资源释放
开发语言·人工智能·数码相机·计算机视觉·c#·视觉检测·机器视觉