起因:一个不该出现的"AI生成"
今天给 AR 绘图桌小程序(Taro 4 + Vue3 微信小程序,提供 AI 生图与图片转线稿能力)做去水印验证,顺手挖出一个之前完全没意识到的坑。
AR 绘图桌的核心玩法之一是让用户上传图片、本地 canvas 经 Otsu 二值化转成黑白线稿,再叠在摄像头画面上照着描。近期接入 AI 生图后,同事反馈:转出来的线稿里偶尔出现一行奇怪的文字——"AI生成"。
我做了 10 年前端,带过猫眼、新东方的业务线,现在也还在做美团小程序和鸿蒙外包。这种"上游产物在下游变形"的问题,在复杂链路里太常见了,但每次都值得认真记一笔。
排查:从二值化一路回溯到生图阶段
第一反应是二值化阈值没调好,把 Otsu 的参数试了一圈,线稿质量有变化,但那行字纹丝不动。冷静下来往上游看:线稿的源图是 AI 生成的,于是去翻生图返回图——右下角赫然压着一行"图片由 AI 生成"。
根因清楚了:这是腾讯云的显式水印标识,官方默认添加,不是我们加的。它是深色文字压在浅色底上,灰度值低;而 Otsu 二值化按全局阈值把像素切成前景(黑)/背景(白),低灰度的水印文字被归入前景,于是原样保留成了线稿里的笔画。
一句话总结这个坑:水印不是"画上去的装饰",而是会被下游图像算法当成真实内容的噪声。
方案:探针式传参 + 防御式回退
官方其实给了开关——TC3 网关层 API 有 LogoAdd / LogoParam,可以关掉或替换 logo。但微信 cloud.ai 的 SDK 文档没暴露这两个参数。
经验告诉我,这条链路会透传不少"文档没写但能生效"的字段(seed、footnote、enable_thinking 之前都验证过),于是我采用探针式传参,试探 footnote: '' 和 logo_add: 0:
// cloud/functions/generateImage/index.js
const input = {
model: 'hunyuan-image',
prompt,
size: '1024x1024',
footnote: '', // 试探:清空水印注解
logo_add: 0, // 试探:关闭官方 logo
};
let noWm = true;
try {
res = await imageModel.generateImage({ ...input });
} catch (e) {
noWm = false;
console.warn('去水印参数被拒,回退带水印重试', e);
res = await imageModel.generateImage({ model: 'hunyuan-image', prompt, size: '1024x1024' });
}
console.log('[generateImage] noWm =', noWm);
这里最重要的不是"怎么传参",而是防御式回退:footnote: '' 有被厂商校验拒绝的风险(文档把它定义为"水印",没说能为空)。一旦拒绝,整次调用就失败——我不能让一个"去水印"的实验把已经跑通的生图主功能搞挂。所以失败就去掉这两个字段、用原参数重生生图。
判读靠云端日志的 noWm 标记,做成四象限:日志为真且图上无水印=生效;日志为真但图上还有=字段名不对;日志为假(有 warn)且图上还有=参数被拒但回退成功。
合规:标识不能丢,但别烧进像素
国内《人工智能生成合成内容标识办法》2025-09-01 施行,要求 AI 内容有显式标识。所以我没走"直接把水印烧掉"的野路子。
最终方案分三层:
- 图片层面去除像素水印,保证转线稿干净;
- 入库时写入
aiGenerated: true元数据,标识随数据走; - 前端 UI 层绘制一个"AI 生成"角标,标识留在明面上。
这样既满足合规,又不会让水印污染下游的二值化与识别。
方法论沉淀
这次排查给我一个更通用的提醒:AI 产物不是流程的终点。它还要被二值化、被 OCR、被下游模型吃。任何"看似无害的附加信息"——水印、边框、EXIF、隐式 padding——都可能在下游被放大成缺陷。
写 AI 管线时,别只盯"能不能出图"。要站在下游消费者的视角,反向约束上游每一个字段。这是把"AI 能力"做成"AI 工程"的分水岭,也是我在面试里最想讲清楚的那种工程素养。
相关:AR 绘图桌项目(Taro 4 + Vue3)、博客 https://blog.alleria.cn