把背景抠掉的透明数字人,在和游戏画面拼 PIP 的时候,沿人物轮廓描一圈白色粗边;圆形浮窗也能加可开关的描边。两个需求本地用纯 ffmpeg 全跑通了。
这次要做的两件事,本质都是"沿一个形状描一圈边":一个形状是人物剪影(扣图轮廓),一个形状是浮窗容器(圆/方)。两者都不需要 AI、不需要额外服务,ffmpeg 一条滤镜链就能出,Rendi 现成能跑。粗细 = 一个像素参数,颜色 = 一个颜色参数,自然语言可以直接映射。
| 需求 | 描边对象 | 触发场景 | 默认 |
|---|---|---|---|
| ① 扣图人物描边 | 人物身体轮廓(跟着头发/肩膀/手走) | 透明扣图人物参与 PIP 合成时 | 白色 · 粗线 |
| ② 浮窗描边 | 浮窗容器形状(圆形 / 异形) | 非上下分屏、是 overlay 圆形/异形浮窗时 | 不描边;开启后默认白色粗线 |
需求 ② 要支持自然语言控制:是否描边、描边颜色都可以用一句话说(如"给浮窗加个红色描边")。下面分别给实现原理和实测画面。
fal 输出的是 VP9 webm 带 alpha(文件头 ALPHA_MODE=1)。但 ffmpeg 默认的原生 vp9 解码器会把 alpha 丢掉,解出来是不透明的 rgb24,alphaextract 直接报 "Requested planes not available"。
必须显式指定 libvpx-vp9 解码器,才能拿到 rgba(带透明):
# 错:原生 vp9 解码器丢 alpha → rgb24
ffmpeg -i in.webm ...
# 对:libvpx-vp9 才解出 rgba(透明保留)
ffmpeg -c:v libvpx-vp9 -i in.webm ...
libvpx-vp9。这是唯一一个不写就直接坏的点。
四步,全是 ffmpeg 滤镜:
[0:v]format=rgba,split[p][a];
[a]alphaextract,dilation×N[edge]; # 剪影向外膨胀 N 像素
color=white:s=WxH,format=rgba[c];
[c][edge]alphamerge[stroke]; # 膨胀剪影填白
[bg][stroke]overlay[t];[t][p]overlay # 描边在下,原人物盖上
前 / 后(深色背景上,12px 白色粗线)


color=white 换成 color=0xFFD400 就是黄色描边。"黄色描边/红色描边"这种自然语言只要映射到一个颜色值。粗细同理 = 膨胀像素数。同一个"描边 = 膨胀形状垫底"的思路,只是形状换成圆(异形同理,换 mask 公式):一块白圆盘垫底,人物裁成略小的圆盖上,只露出外圈 = 白色粗环。默认不描边,开启后默认白色粗线。


建议在 pip / overlay 配置下加一个 stroke 块,自然语言由 Mai gateway 映射成字段:
"pip": {
"layout": "overlay",
"overlay_shape": "circle",
"stroke": {
"enabled": false, # 需求②默认 false;需求①(扣图人物)可默认 true
"color": "white", # 自然语言→颜色值,默认白
"width_px": 12 # "粗线"≈10–14px
}
}
| 自然语言 | → 字段 |
|---|---|
| "给浮窗加描边" | stroke.enabled=true |
| "红色描边" / "描边用品牌色" | stroke.color="red" / "#1B365D" |
| "细一点 / 更粗" | stroke.width_px 调小/调大 |
| (不提) | 需求②保持 enabled=false;需求①人物描边按你定的默认开 |
哪些 layout 生效:需求①的身体描边适用于任何用透明扣图人物的布局;需求②的浮窗环只在 overlay(圆/异形)生效,split_v(上下分屏)不涉及。
libvpx-vp9,否则 alpha 丢失(唯一硬坑)。