--- tags: [image-processing, sprite-sheet, gif, computer-vision, go, algorithm] create time: 2026-06-03 10:25 --- # 06. 精灵图处理管线 ## 概述 自动精灵图处理管线 — 背景移除 → 投影检测 → 切割 → 对齐 → GIF 预览,将一张 AI 生成的 Sprite Sheet 无缝转化为可用的逐帧动画资源。 --- ## 正文 ```mermaid flowchart LR A["Input PNG"] --> B["Background Removal"] B --> C["Gap Detection"] C --> D["Tile Extract"] D --> E["Filter MinFill"] E --> F["Trim Alpha"] F --> G["Align padToLargest"] G --> H["GIF Preview"] style A fill:#e3f2fd,stroke:#1976d2 style B fill:#fff3e0,stroke:#f57c00 style C fill:#e8f5e9,stroke:#388e3c style D fill:#fce4ec,stroke:#c62828 style E fill:#f3e5f5,stroke:#7b1fa2 style F fill:#e0f7fa,stroke:#00838f style G fill:#fff8e1,stroke:#f9a825 style H fill:#e8eaf6,stroke:#303f9f ``` --- ## 处理管线总览 AI 生成的 Sprite Sheet 通常包含白色/绿色背景、不均匀间距和尺寸差异。Gen2D 的精灵图处理管线自动完成从"原始 PNG"到"可用动画帧"的全部转换工作,无需用户手动操作。 **管线核心入口**:`splitsprite.Process(img, opts)` | 步骤 | 函数 | 作用 | |:---:|------|------| | 1 | `removeWhiteBg` / `removeGreenScreen` | 移除背景色,生成透明通道 | | 2 | `projectionSplit` / `fixedGridSplit` | 检测帧边界,确定切割位置 | | 3 | `MinFillRatio` 过滤 | 丢弃空白/低填充率的无效帧 | | 4 | `trimAlpha` | 去除每帧的透明边框,减少冗余像素 | | 5 | `padToLargest` | 统一画布尺寸,底部居中对齐 | --- ## 步骤 1:背景移除 AI 生成的图片通常带有纯色背景,管线支持两种模式: ### 白底模式(WhiteBg) ```go // 阈值距离 #FFFFFF + alpha 渐变 dist := max(255-R, max(255-G, 255-B)) if dist < threshold/2 → 全透明 if dist < threshold → alpha 线性渐变(抗锯齿) ``` - **WhiteThreshold** 默认 40,距离纯白 40 以内的像素被处理 - 采用渐变 alpha 实现平滑过渡,避免硬边缘 ### 绿幕模式(GreenScreen) ```go // 绿色主导检测 gDominance := G - (R+B)/2 if gDominance > tolerance*255 → 透明化 ``` - **GreenTolerance** 默认 0.2,控制绿色检测灵敏度 - 适用于绿色背景的 AI 生成图 > [!tip] 设计选择 > 两种模式互斥,`Process()` 根据 `opts.WhiteBg` / `opts.GreenScreen` 自动选择。 --- ## 步骤 2:切割策略 管线提供两种切割方式,根据配置自动切换: ### 投影检测(Projection Detection) 这是 Gen2D 的核心创新点。传统全局投影在检测列边界时,会因为武器、尾巴、翅膀等突出物被"稀释"而导致切割不准。 **两阶段投影算法**: 1. **全局行投影**:统计每行非透明像素占比,检测水平间隙 - 行间隙通常是干净且全宽的,全局投影效果好 2. **逐行列投影**:在每个行段内独立计算列密度 - 防止突出物(武器/尾巴)被其他行稀释 - 每行段获得独立的列边界,互不干扰 > [!question] 为什么不用简单的阈值分割? > > 精灵图中的角色往往有复杂轮廓——比如挥舞的剑可能横跨多个帧的位置。如果仅用固定阈值,剑的连续像素会让算法误判为一帧。**按行段独立分析**的思路是把二维问题拆解为多个一维子问题,每个子问题只关心当前行段的内容,从而避免跨行干扰。 ```mermaid flowchart TB A["输入图像"] --> B["全局行投影
检测行间隙"] B --> C["行段 1"] B --> D["行段 2"] B --> E["行段 N"] C --> F["逐行列投影"] D --> G["逐行列投影"] E --> H["逐行列投影"] F --> I["tiles[]"] G --> I H --> I style B fill:#e8f5e9,stroke:#388e3c style F fill:#fff3e0,stroke:#f57c00 style G fill:#fff3e0,stroke:#f57c00 style H fill:#fff3e0,stroke:#f57c00 ``` **峰值检测算法**(`findCuts`): - 对密度曲线做滑动平均平滑(kernel = minGap) - 以均值为参考检测峰值段(start: mean*1.05, continue: mean*0.95) - 在相邻峰之间的谷底确定切割位置 - 回退机制:峰值不足时降级到阈值法(`findCutsByGap`) ### 固定网格(Fixed Grid) 当 `GridRows > 0 && GridCols > 0` 时启用,将图像等分为 Rows×Cols 个单元格。 - **GridPadding**:单元格间间距(默认 2px) - 适用于已知行列数的标准 Sprite Sheet --- ## 步骤 3:过滤 — MinFillRatio 切割后的每个 tile 都计算填充率: ``` fillRatio = 非透明像素数 / 总像素数 ``` - **MinFillRatio** 默认 0.14(14%) - 低于阈值的 tile 被判定为空白帧,自动丢弃 - 有效过滤因间隙检测误差产生的空白切片 --- ## 步骤 4:裁剪 — trimAlpha 对每个 tile 执行透明边框裁剪: 1. 扫描四边界,找到非透明像素的最小包围矩形 2. 裁切到该矩形,去除四周透明区域 3. 减少冗余像素,为后续对齐做准备 --- ## 步骤 5:对齐 — padToLargest 动画播放时,如果每帧尺寸不同且内容未对齐,会导致角色"抖动"。 **底部居中锚定**(Bottom-Center Anchor): ``` canvasW = maxW * 110% // 最大帧宽度 + 10% padding canvasH = maxH * 110% // 最大帧高度 + 10% padding 每帧偏移: ox = (canvasW - frameW) / 2 // 水平居中 oy = canvasH - frameH // 底部对齐(脚踏同一水平线) ``` - 所有帧共享统一画布尺寸 - 水平居中保证角色在同一屏幕位置 - 底部对齐保证角色"脚踏实地",防止上下漂移 --- ## GIF Maker `gifmaker.Encode()` 将处理后的帧序列编码为动画 GIF: | 特性 | 实现 | |------|------| | 统一画布 | 所有帧归一化到 maxW × maxH | | 透明色 | 调色板索引 0 = 完全透明 | | 防鬼影 | `DisposalBackground` 每帧清除前一帧 | | 调色板 | 采样像素构建(每 3px 取样),最多 256 色 | | 循环 | `LoopCount = 0`(无限循环) | ```go anim.Disposal = append(anim.Disposal, gif.DisposalBackground) anim.BackgroundIndex = 0 // 透明色 ``` > [!warning] DisposalBackground 的重要性 > 如果不设置此选项,GIF 播放器会在前一帧基础上叠加新帧,产生"残影"效果。 --- ## Options 配置速查 | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `WhiteBg` | bool | true | 白底移除模式 | | `WhiteThreshold` | uint8 | 40 | 白色阈值(0-255) | | `GreenScreen` | bool | false | 绿幕移除模式 | | `GreenTolerance` | float64 | 0.2 | 绿色容差(0-1) | | `GridRows` / `GridCols` | int | 0 | 固定网格行列数 | | `GapThreshold` | float64 | 0.03 | 间隙判定阈值 | | `MinGapWidth` | int | 2 | 最小间隙宽度(px) | | `MinFillRatio` | float64 | 0.14 | 最小填充率 | | `Trim` | bool | true | 透明边框裁剪 | | `CenterAlign` | bool | true | 底部居中对齐 | --- ## 关联文档 - [[00-索引]] — 文档导航与架构总览图 - [[05-生成管线]] — 管线中 SplitSprite 节点的调用方 - [[08-SSE实时推送]] — 处理进度的实时推送