# 素材管线实测报告 > 记录 `assets/cook-已抠图/*.mp4` → `assets/pet/*.webm` 这条管线**实测出来的**数据, > 以及每个数字背后的判断依据。所有数值都是在本机跑出来的,不是估算。 > > 复现:`npm run assets`(生成)+ `npm run assets:check`(验收)。 --- ## 1. 源素材是什么 6 段 AI 生成的 1280×720 / 30fps H.264 mp4,"已抠图"指的是**背景被换成纯黑**, 但 **alpha 通道在导出时已经丢了**(H.264 不支持 alpha)。 | 动作 | 输出文件 | 帧数 | 时长 | 说明 | | --- | --- | --- | --- | --- | | 待机 | `idle.webm` | 302 | 10.07s | 持铲站立、轻微晃动 | | 颠锅 | `cook.webm` | 302 | 10.07s | 回合进行中 | | 上菜 | `serve.webm` | 142 | 4.73s | 回合完成 | | 托腮 | `think.webm` | 152 | 5.07s | **等你确认/回答** | | wink | `wink.webm` | 83 | 2.77s | 点击回应 | | 转圈 | `spin.webm` | 152 | 5.07s | 点击回应 | ### 逐段实测(阈值:`max(B,G,R) > 12`,每 2 帧采样) | 动作 | alpha 覆盖率 | 并集 bbox(原图坐标) | 帧间中心跳变 >40px | 贴边帧数(顶/底/左/右) | | --- | --- | --- | --- | --- | | idle | 17.04% | (428, 19)–(857, 702) | 0 | 0 / 0 / 0 / 0 | | cook | 22.55% | (286, 17)–(897, 719) | 0 | 0 / 4 / 0 / 0 | | serve | 18.62% | (435, 0)–(885, 719) | 0 | **142** / 21 / 0 / 0 | | think | 17.20% | (424, 17)–(850, 702) | 0 | 0 / 0 / 0 / 0 | | wink | 17.58% | (348, 24)–(887, 702) | 0 | 0 / 0 / 0 / 0 | | spin | 18.67% | (320, 0)–(996, 707) | **2**(44.6px / 43.0px) | 9 / 0 / 0 / 0 | ### 两处"不完美"是素材原生问题,**没有去修** - **`上菜` 全部 142 帧的顶边都有内容**(顶边内容宽度均值 42px、最大 260px): 端起的盘子被原画幅顶端切掉了一小截。修它需要重新生成素材。 - **`转圈` 在跳跃顶点有 2 次约 44px 的 bbox 抖动**:同样来自"跳起来时头顶/盘子出画"。 - 这两处在 138px 的显示尺寸下看不出来;**明确保留原样,不做补边/插帧/稳定化**。 --- ## 2. 裁切框(6 段共用) 切换动作时人物不能跳位,所以 6 段必须用**同一个**裁切框: ``` 全 6 段所有帧的 alpha 并集 = x∈[286,996]、y∈[0,719] 取整并留余量 → CROP = (280, 0, 1003, 720) 原图 723×720 输出高度 512 → 宽度 round(723 × 512/720) = 514 ``` 输出 **514×512(宽高比 1.0039)**。 > ⚠️ 改裁切框会连带改 `runtime/electron-helper/renderer.js` 顶部的命中框比例常量, > 两处必须一起改。 `idle` 的人物在该框内的相对位置(实心 `alpha>200` 口径,桌面窗命中判定用): | left | top | width | height | | --- | --- | --- | --- | | 0.2237 | 0.0332 | 0.5603 | 0.9414 | --- ## 3. alpha 管线:四代实现与实测数据 这条管线改了三轮才把边缘做干净。**每一步的差值都是量出来的**,不是"看着差不多": | 版本 | 做法 | 边缘反预乘亮度 | 问题 | | --- | --- | --- | --- | | v1 | 二值 mask + 高斯羽化 | **0.2** / 255 | 羽化带的 RGB 还是黑底 → 一圈脏黑边 | | v2 | 3px 邻域**均值**扩散 | **0.6** | 均值被黑底稀释,几乎没改善 | | v3-错误 | `distanceTransformWithLabels` 求最近邻 | **24.7** | 该 API 的标签语义与直觉相反,取色错位成黑 | | **v4(采用)** | `scipy.distance_transform_edt` 最近邻**复制** | **101.9 ~ 116.6** | ✅ 与内部亮度 175 的差距属正常抗锯齿 | **关键诊断方法**:打印一条穿过人物边缘的水平扫描线,逐像素看 `alpha / 原始RGB / 外扩后RGB`。 v3-错误版里连 `alpha=255` 的实心像素 RGB 都是 0 —— 这个现象直接证明"取色映射全错", 而不是"扩散不够"。换成 v4 后同一位置变成 `a=228 → RGB[59,70,85]`(人物自己的颜色)。 ### 一个 API 陷阱(记下来避免再踩) `cv2.distanceTransformWithLabels(..., labelType=cv2.DIST_LABEL_PIXEL)` **不是** "每个源点一个编号"。实测 6×6 图、2 个源点,返回的 labels 是 `1..34`(逐像素编号), 按直觉建映射表必然取错色。 --- ## 4. 颜色通道(最隐蔽的 bug) v3 曾经把 BGR 数据按原顺序塞进标记为 RGBA 的编码管道,结果是: ``` 源图红围裙 BGR = [94, 93, 218] → R=218(红) v3 错误成品 BGRA = [219, 90, 95] → B=219(红跑到蓝通道) v4 修复后 RGBA = [218, 93, 94] → R=218 ✅ ``` **为什么差点放过**:alpha、尺寸、覆盖率、四角透明度全部正常,只有颜色反了。 肉眼看到的是"红围裙变紫、棕发变蓝",很容易误判成"素材本身配色不对"。 **防守手段**:`scripts/check-assets.py` 里加了"红度"回归检查 —— `人物区域的 mean(R) - mean(B)`,原片约 `+41~+51`,一旦 R/B 互换就翻成 `-41~-51`。 用旧的问题素材做过负向测试,能稳定报错并退出码 1。 --- ## 5. 最终验收数据(`npm run assets:check`) | 动作 | 尺寸 | 大小 | 透明% | 不透明% | 四角 alpha | 内部 255% | 暗部 alpha | 红度(原片/成品) | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | idle | 514×512 | 1279 KiB | 69.4 | 28.9 | [1,1,1,1] | 28.6 | 255 | 41 / 41 | | cook | 514×512 | 2035 KiB | 58.1 | 40.3 | [1,1,1,1] | 40.5 | 255 | 44 / 43 | | serve | 514×512 | 1146 KiB | 66.0 | 32.4 | [1,1,1,1] | 32.0 | 255 | 51 / 51 | | think | 514×512 | 583 KiB | 68.8 | 29.7 | [1,1,1,1] | 29.5 | 255 | 42 / 42 | | wink | 514×512 | 373 KiB | 68.9 | 29.5 | [1,1,1,1] | 29.4 | 255 | 43 / 43 | | spin | 514×512 | 1130 KiB | 66.0 | 32.2 | [1,1,1,1] | 31.9 | 255 | 42 / 42 | - **四角 alpha ≈ 0**:背景确实透明了(1/255 是羽化+色度二次采样的残差,不可见)。 - **暗部 alpha = 255**:黑色锅铲与深棕描边**没有被误删**。 - **6 段尺寸完全一致**:切换动作不跳位。 - **合计 6.4 MiB**(随插件一起发布)。 与源素材的颜色保真(逐帧解码比对,人物区域): | 动作 | 色差 MAE | PSNR | | --- | --- | --- | | idle | 4.55 / 255 | 35.0 dB | | cook | 4.43 / 255 | 35.2 dB | | serve | 4.70 / 255 | 34.7 dB | | think | 4.50 / 255 | 35.1 dB | | wink | 4.34 / 255 | 35.4 dB | | spin | 4.85 / 255 | 34.4 dB | --- ## 6. 浏览器侧的两个坑(不是素材问题,但会导致"换了素材看不到变化") 1. **长缓存**:`/asset/*` 带 `cache-control: public, max-age=86400`(素材是静态的,合理)。 代价是改完素材后浏览器 24 小时内仍用旧文件。 解法:桌面窗的素材 URL 统一带 `?v=`(`runtime/electron-helper/renderer.js`),换素材时把这个号 +1。 2. **桌面窗在内存里**:Electron helper 启动时就把 `idle.webm` 读进 `