230 lines
7.0 KiB
Markdown
230 lines
7.0 KiB
Markdown
---
|
||
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["全局行投影<br/>检测行间隙"]
|
||
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实时推送]] — 处理进度的实时推送
|