
前言:
前段时间给客户做了一款线下活动小程序,里面有个需求:用户在现场拍一张人像,系统自动把人抠出来,合成到活动背景上,生成一张带 AI 标识的打卡合照。说白了就是一个"AI 打卡合照"功能。
我调研了几套方案:
- 非主流方法:纯前端的方式找开源抠图模型,复制或安装到项目之中进行使用,最后就是折腾半天没成功过...
- 主流办法 :Ai抠图模型的使用(腾讯云人体分析 BDA)
SegmentPortraitPic接口专门做人像分割,按量计费(每月有赠送1000的请求抠图次数),返回支持url,和因为小程序用的就是云服务所以搭配腾讯人体分析有互通使用起来还是不错,
一、最终效果与整体流程
页面操作流程:
选择活动背景 → 拍照 / 从相册选人像 → 等待 AI 抠图 → 拖拽缩放旋转人像 → 保存打卡合照
技术层面:
小程序端拍照/选图
↓
智能压缩(超过 3.5MB 压到 1920px)
↓
上传图片到小程序云存储
↓
调用云函数 segment-portrait
↓
云函数下载图片 → 调腾讯云 SegmentPortraitPic → 拿到抠图结果 url
↓
下载抠图结果 → 上传回云存储 → 返回 fileID
↓
小程序端获取临时 url → Canvas 合成背景+人像
↓
嵌入 AI 生成水印 → 保存到相册
为什么要把 AI 接口放在云函数里?
SecretId/SecretKey不能暴露在前端。- 腾讯云 API 需要 TC3-HMAC-SHA256 签名,云函数里用 Node.js
crypto原生成,零依赖。- 大图片 base64 传输容易超限,云函数里走
url输出类型更稳。(其实最开始我就是本地项目,直接请求抠图ai模型的导致传输内容太大抠图失败,除非使用很1MB内的图片,否则很容易抠图失败)
二、腾讯云:开通人体分析并拿到密钥【开通子账户,并设置对应抠图权限】
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 一闪而过,必须用这个自定义错误框把原因讲清楚。
九、踩坑总结
- 不要从前端直接调腾讯云 API:密钥会泄露,而且前端签名校验环境不稳定。
- AI 接口返回用 url,不要用 base64:base64 大图直接打爆云函数返回限制。
- 云函数内存开到 512MB:处理高清图片时 128MB/256MB 很容易 OOM。
- 背景图不要打进分包:高清背景图体积大,建议放云存储或 CDN,按 url 下载。
- 水印必须嵌入合成图:只做页面 overlay 审核过不了。
- 用户取消选图不算错误 :
errMsg含cancel时静默处理。 - 抠图结果 url 有效期短:云函数下载后再传回云存储,前端用云存储临时 url 更稳。
- 分包大小控制在 2MB 以内:如果背景图很多,考虑按需下载,不要预置在包里。
十、结语
整个方案走下来,核心就是三段:前端负责选图压缩和交互、云函数负责安全签名和 AI 调用、Canvas 负责合成输出 。腾讯云 SegmentPortraitPic 的抠图效果对于打卡合照这种场景完全够用,头发丝级别的精细度可能比不上专业修图软件,但活动现场快速生成一张可用合照绰绰有余。
如果你也在做类似功能,建议先把云函数签名调通,再拼前端 Canvas,最后处理水印和审核细节。祝大家一次过审。
标签:

