# sprite-slicer
`sprite-slicer` 是一个面向 2D 游戏素材流程的 Rust crate,既能作为命令行工具使用,也能作为库嵌入到你自己的工具链中。
它解决的是一条完整的 sprite 处理链路:
- 把规则 sprite sheet 按网格切成单帧
- 从透明背景图中自动检测单帧
- 按动作把帧重新分组
- 去除黑色背景并转透明 PNG
- 把一组帧统一到同一画布和锚点
- 导出 GIF 预览动画
仓库地址:
- GitHub: <https://github.com/mdddj/sprite-slicer>
- docs.rs: <https://docs.rs/sprite-slicer>
## 适用场景
- AI 生成了一整张角色动作图,需要切成单帧
- 一张图里角色站位不规则,需要自动检测角色块
- 想把帧按 `idle / walk / attack / hurt` 分到不同目录
- 想把黑底或近黑底图片转成透明背景 PNG
- 想把动作帧对齐到统一尺寸,避免游戏里角色抖动
- 想快速导出 GIF 看动画节奏是否正常
## 特性
- 同时提供 `CLI` 和 `library API`
- 输入输出都是文件和目录,适合脚本、工具链、AI agent 调用
- 支持规则网格切图和透明图自动检测
- 支持动作分组导出
- 支持背景去除和帧规范化
- 支持 GIF 预览导出
- 适合像素角色、敌人动画、JRPG sprite workflow
## 安装
作为库:
```bash
cargo add sprite-slicer
```
作为命令行工具:
```bash
cargo install sprite-slicer
```
## 目录约定
这个库是“文件导向”的。大多数 API 都会:
- 从一个图片文件或目录读入
- 在输出目录里生成 PNG、TOML、TXT、GIF
- 返回一个包含产物路径和统计信息的结果结构体
默认产物通常包括:
- `frames/`:切出来的 PNG 单帧
- `frames.toml`:帧清单 manifest
- `index-map.txt`:便于肉眼核对帧序号的文本图
- `actions.toml`:动作分组后的摘要清单
## 给 AI 调用的建议
如果你后续要让别的 AI 直接操作这个库,建议固定用下面这套顺序:
1. 如果原图是黑底或纯色底,先调用 `remove_background`
2. 如果是规则网格图,调用 `slice_sheet`
3. 如果不是规则网格、已经是透明底,调用 `detect_frames`
4. 根据 `frames.toml` 生成动作配置,再调用 `group_actions`
5. 对每个动作目录调用 `normalize_frames`
6. 对每个动作目录调用 `export_gif` 做预览
建议 AI 遵循这些约束:
- 所有输入路径都使用绝对路径
- 输出目录不要复用未清理的旧结果目录
- `group_actions` 之前必须确保 `frames.toml` 已生成
- `normalize_frames` 和 `export_gif` 只接受 PNG 文件或只包含 PNG 的目录
- 对 AI 生成图优先尝试 `detect_frames`,对规则表格图优先尝试 `slice_sheet`
## AI 调用模板
下面这段可以直接复制给别的 AI,当作它操作这个库的规则。
```text
你正在操作一个 Rust 库:sprite-slicer。
目标:
- 处理 2D sprite sheet
- 产出单帧 PNG、动作目录、规范化帧和 GIF 预览
公开函数:
- slice_sheet(SliceOptions) -> Result<SliceOutput>
- detect_frames(DetectOptions) -> Result<DetectOutput>
- group_actions(GroupOptions) -> Result<GroupOutputSummary>
- normalize_frames(NormalizeOptions) -> Result<NormalizeOutput>
- export_gif(GifOptions) -> Result<GifOutput>
- remove_background(RemoveBgOptions) -> Result<RemoveBgOutput>
工作流规则:
1. 如果输入图是黑底或纯色背景,先调用 remove_background。
2. 如果图像是规则网格,调用 slice_sheet。
3. 如果图像不是规则网格但背景透明,调用 detect_frames。
4. 必须根据 frames.toml 中的 frame index 来生成 group_actions 的配置。
5. 动作分组完成后,对每个动作目录调用 normalize_frames。
6. 预览动画时,对规范化后的动作目录调用 export_gif。
路径规则:
- 输入输出路径尽量使用绝对路径。
- 不要把不同步骤的结果写到同一个未清理目录。
- normalize_frames 和 export_gif 的输入必须是 PNG 文件或仅包含 PNG 的目录。
- 目录输入时,这个库只扫描当前目录下一层 PNG,不做递归扫描。
参数选择建议:
- 黑底去背:bg_hex = "#000000",threshold 从 8 开始尝试。
- AI 生成图自动检测:min_opaque_pixels 从 64 开始,padding 从 2 开始,row_tolerance 从 24 开始。
- 角色动画对齐:anchor_x = Center,anchor_y = Bottom,pad = 2 或 4。
- GIF 预览:fps = 8 或 12,repeat = 0。
错误处理建议:
- 如果 detect_frames 报 no components matched,降低 min_opaque_pixels,或调整 alpha/background threshold。
- 如果 group_actions 报 frame index 不存在,检查动作配置里的索引是否来自 frames.toml。
- 如果 group_actions 报 frame was not exported,说明切图时 skip_empty 把该帧跳过了。
- 如果 export_gif 或 normalize_frames 报 no png frames found,检查输入目录是否真的有 PNG。
```
## 公开 API 总览
当前公开函数一共 6 个:
- `slice_sheet(options: SliceOptions) -> Result<SliceOutput>`
- `detect_frames(options: DetectOptions) -> Result<DetectOutput>`
- `group_actions(options: GroupOptions) -> Result<GroupOutputSummary>`
- `export_gif(options: GifOptions) -> Result<GifOutput>`
- `remove_background(options: RemoveBgOptions) -> Result<RemoveBgOutput>`
- `normalize_frames(options: NormalizeOptions) -> Result<NormalizeOutput>`
- `process_sprite_sheet(options: ProcessSheetOptions) -> Result<ProcessSheetOutput>`
可直接引入:
```rust
use sprite_slicer::{
detect_frames, export_gif, group_actions, normalize_frames, process_sprite_sheet,
remove_background, slice_sheet, AnchorX, AnchorY, ComponentMode, DetectOptions, FrameAlign,
GifOptions, GroupOptions, NormalizeOptions, ProcessSheetOptions, RemoveBgOptions,
SliceOptions,
};
```
## 快速示例
### 1. 黑底转透明
```rust
use std::path::PathBuf;
use sprite_slicer::{remove_background, RemoveBgOptions};
fn main() -> anyhow::Result<()> {
let output = remove_background(RemoveBgOptions {
input: PathBuf::from("sheet-black.png"),
output: PathBuf::from("sheet-transparent.png"),
bg_hex: "#000000".to_string(),
threshold: 8,
alpha_threshold: 0,
})?;
println!("removed {} pixels", output.removed_pixels);
Ok(())
}
```
### 2. 规则网格切图
```rust
use std::path::PathBuf;
use sprite_slicer::{slice_sheet, SliceOptions};
fn main() -> anyhow::Result<()> {
let output = slice_sheet(SliceOptions {
input: PathBuf::from("hero-sheet.png"),
output: PathBuf::from("out/slice"),
frame_width: 64,
frame_height: 64,
columns: Some(8),
rows: Some(6),
offset_x: 0,
offset_y: 0,
gap_x: 0,
gap_y: 0,
skip_empty: true,
alpha_threshold: 0,
min_opaque_pixels: 1,
bg_hex: None,
bg_threshold: 0,
manifest_name: "frames.toml".to_string(),
})?;
println!("manifest: {}", output.manifest_path.display());
println!("index map: {}", output.index_map_path.display());
println!("frame count: {}", output.frame_count);
println!("kept frames: {}", output.kept_frames);
Ok(())
}
```
### 3. 透明图自动检测
```rust
use std::path::PathBuf;
use sprite_slicer::{detect_frames, DetectOptions};
fn main() -> anyhow::Result<()> {
let output = detect_frames(DetectOptions {
input: PathBuf::from("sheet-transparent.png"),
output: PathBuf::from("out/detect"),
alpha_threshold: 0,
min_opaque_pixels: 64,
padding: 2,
row_tolerance: 24,
bg_hex: None,
bg_threshold: 0,
manifest_name: "frames.toml".to_string(),
})?;
println!("detected frames: {}", output.detected_frames);
println!("rows: {}", output.rows);
Ok(())
}
```
### 4. 按动作分组
```rust
use std::path::PathBuf;
use sprite_slicer::{group_actions, GroupOptions};
fn main() -> anyhow::Result<()> {
let output = group_actions(GroupOptions {
manifest: PathBuf::from("out/detect/frames.toml"),
config: PathBuf::from("examples/actions.toml"),
output: PathBuf::from("out/actions"),
})?;
println!("actions manifest: {}", output.manifest_path.display());
for action in output.actions {
println!("{} -> {}", action.name, action.frame_count);
}
Ok(())
}
```
### 5. 统一帧尺寸和锚点
### 6. 洋红底 sprite sheet 后处理
这个入口更接近 AI 生成 sprite 的实际落地流程:去洋红背景、切网格、按最大主体或整帧裁剪、统一缩放、重新拼 sheet、导出 GIF,并输出 `pipeline-meta.json`。
```rust
use std::path::PathBuf;
use sprite_slicer::{process_sprite_sheet, ComponentMode, FrameAlign, ProcessSheetOptions};
fn main() -> anyhow::Result<()> {
let output = process_sprite_sheet(ProcessSheetOptions {
input: PathBuf::from("raw-sheet.png"),
output_dir: PathBuf::from("out/processed"),
rows: 2,
cols: 2,
cell_size: 128,
bg_hex: "#FF00FF".to_string(),
threshold: 100,
edge_threshold: 150,
fit_scale: 0.85,
trim_border: 4,
edge_clean_depth: 3,
align: FrameAlign::Center,
shared_scale: true,
component_mode: ComponentMode::All,
component_padding: 0,
min_component_area: 1,
edge_touch_margin: 2,
reject_edge_touch: true,
gif_delay: 20,
frame_labels: None,
prompt: Some("forest spell impact".to_string()),
})?;
println!("sheet: {}", output.sheet_path.display());
println!("gif: {}", output.gif_path.display());
println!("meta: {}", output.metadata_path.display());
println!("frames: {}", output.frame_count);
println!("edge-touch frames: {:?}", output.edge_touch_frames);
Ok(())
}
```
```rust
use std::path::PathBuf;
use sprite_slicer::{normalize_frames, AnchorX, AnchorY, NormalizeOptions};
fn main() -> anyhow::Result<()> {
let output = normalize_frames(NormalizeOptions {
input: PathBuf::from("out/actions/walk"),
output: PathBuf::from("out/normalized/walk"),
width: None,
height: None,
anchor_x: AnchorX::Center,
anchor_y: AnchorY::Bottom,
pad: 4,
})?;
println!("canvas: {}x{}", output.canvas_width, output.canvas_height);
println!("frames: {}", output.frame_count);
Ok(())
}
```
### 6. 导出 GIF 预览
```rust
use std::path::PathBuf;
use sprite_slicer::{export_gif, GifOptions};
fn main() -> anyhow::Result<()> {
let output = export_gif(GifOptions {
input: PathBuf::from("out/normalized/walk"),
output: PathBuf::from("out/previews/walk.gif"),
fps: 8,
repeat: 0,
pad: 4,
})?;
println!("gif: {}", output.output_path.display());
println!("frames: {}", output.frame_count);
Ok(())
}
```
## API 详细说明
### `slice_sheet`
签名:
```rust
pub fn slice_sheet(options: SliceOptions) -> anyhow::Result<SliceOutput>
```
用途:
- 适合规则网格的 sprite sheet
- 按固定宽高、固定行列切出单帧
- 可跳过空白格
`SliceOptions` 字段说明:
- `input`: 输入图片路径
- `output`: 输出目录
- `frame_width`: 单帧宽度
- `frame_height`: 单帧高度
- `columns`: 列数;`None` 时自动推导
- `rows`: 行数;`None` 时自动推导
- `offset_x`: 左侧起始偏移
- `offset_y`: 顶部起始偏移
- `gap_x`: 横向间距
- `gap_y`: 纵向间距
- `skip_empty`: 是否跳过空白帧导出
- `alpha_threshold`: alpha 小于等于该值时当作透明
- `min_opaque_pixels`: 前景像素少于该值时视为空帧
- `bg_hex`: 可选背景色,形如 `#000000`
- `bg_threshold`: 背景颜色容差
- `manifest_name`: 输出 manifest 文件名,通常是 `frames.toml`
输出 `SliceOutput`:
- `manifest_path`: manifest 路径
- `index_map_path`: 文本索引图路径
- `frame_count`: 总格子数
- `kept_frames`: 实际导出的帧数
会生成:
- `output/frames/*.png`
- `output/<manifest_name>`
- `output/index-map.txt`
适合 AI 的判断规则:
- 如果图片是标准行列排布,用它
- 如果帧之间不规则、大小不一致,不要用它
### `detect_frames`
签名:
```rust
pub fn detect_frames(options: DetectOptions) -> anyhow::Result<DetectOutput>
```
用途:
- 适合透明背景图
- 用连通区域检测方式识别每个 sprite
- 自动按行聚类并编号
`DetectOptions` 字段说明:
- `input`: 输入图片路径
- `output`: 输出目录
- `alpha_threshold`: alpha 小于等于该值时视为空气
- `min_opaque_pixels`: 小于该前景像素数的连通块会被忽略
- `padding`: 对检测框四周补边
- `row_tolerance`: 自动分行时允许的垂直容差
- `bg_hex`: 可选背景色;即使不是透明图,也可配合颜色过滤
- `bg_threshold`: 背景颜色容差
- `manifest_name`: 输出 manifest 文件名
输出 `DetectOutput`:
- `manifest_path`: manifest 路径
- `index_map_path`: 文本索引图路径
- `detected_frames`: 检测到的帧数
- `rows`: 聚类出的行数
会生成:
- `output/frames/*.png`
- `output/<manifest_name>`
- `output/index-map.txt`
适合 AI 的判断规则:
- AI 生成图优先尝试这个函数
- 如果报 `no components matched`,通常要降低 `min_opaque_pixels` 或调整阈值
### `group_actions`
签名:
```rust
pub fn group_actions(options: GroupOptions) -> anyhow::Result<GroupOutputSummary>
```
用途:
- 根据动作配置,把切好的帧复制到各自动作目录
- 适合把一堆 `frames/*.png` 重新组织成 `idle/ walk/ attack/`
`GroupOptions` 字段说明:
- `manifest`: `slice_sheet` 或 `detect_frames` 产出的 `frames.toml`
- `config`: 动作配置 TOML 文件
- `output`: 动作输出目录
动作配置格式:
```toml
[[actions]]
name = "idle"
frames = [0, 1, 2, 3]
[[actions]]
name = "walk"
frames = [4, 5, 6, 7]
```
输出 `GroupOutputSummary`:
- `manifest_path`: 输出的 `actions.toml`
- `actions`: 每个动作的摘要列表
每个摘要项 `GroupedActionSummary` 包含:
- `name`: 动作名
- `frame_count`: 帧数量
会生成:
- `output/<action-name>/0000.png`
- `output/<action-name>/0001.png`
- `output/actions.toml`
注意:
- `frames` 里填的是 `frames.toml` 里的帧索引,不是文件名
- 如果某帧在切图阶段因为 `skip_empty` 没导出,这里会报错
### `normalize_frames`
签名:
```rust
pub fn normalize_frames(options: NormalizeOptions) -> anyhow::Result<NormalizeOutput>
```
用途:
- 把一组 PNG 帧放到同样大小的画布上
- 用统一锚点对齐,减少游戏里跳动感
`NormalizeOptions` 字段说明:
- `input`: 输入 PNG 文件或目录
- `output`: 输出目录
- `width`: 目标宽度;`None` 时使用输入帧中的最大宽度
- `height`: 目标高度;`None` 时使用输入帧中的最大高度
- `anchor_x`: 水平锚点,支持 `Left / Center / Right`
- `anchor_y`: 垂直锚点,支持 `Top / Center / Bottom`
- `pad`: 每边额外留白
输出 `NormalizeOutput`:
- `output_dir`: 输出目录
- `frame_count`: 输出帧数
- `canvas_width`: 最终画布宽度
- `canvas_height`: 最终画布高度
- `anchor_x`: 实际使用的水平锚点
- `anchor_y`: 实际使用的垂直锚点
输入规则:
- 传文件时,必须是 PNG
- 传目录时,只会扫描该目录下一层的 PNG,不会递归
适合游戏导入的默认建议:
- 角色动画推荐 `AnchorX::Center + AnchorY::Bottom`
- 顶视角特效可考虑 `Center + Center`
### `export_gif`
签名:
```rust
pub fn export_gif(options: GifOptions) -> anyhow::Result<GifOutput>
```
用途:
- 从单个 PNG 或一个 PNG 目录生成 GIF 预览
- 方便先肉眼检查动作节奏
`GifOptions` 字段说明:
- `input`: 输入 PNG 文件或目录
- `output`: 输出 GIF 路径
- `fps`: 帧率,最小按 1 处理
- `repeat`: 重复次数,`0` 表示无限循环
- `pad`: 给 GIF 画布四周加留白
输出 `GifOutput`:
- `output_path`: GIF 路径
- `frame_count`: 帧数
- `canvas_width`: GIF 画布宽度
- `canvas_height`: GIF 画布高度
- `fps`: 实际使用的帧率
输入规则:
- 传目录时,只读取该目录下一层的 PNG
- 文件名会先排序,再按顺序组成 GIF
建议:
- 最好先用 `normalize_frames`,再导出 GIF
- 这样每帧尺寸一致,预览更稳定
### `remove_background`
签名:
```rust
pub fn remove_background(options: RemoveBgOptions) -> anyhow::Result<RemoveBgOutput>
```
用途:
- 把和边界连通的背景色抠掉,转成透明
- 适合黑底、纯色底、近纯色底 sprite 图
`RemoveBgOptions` 字段说明:
- `input`: 输入图片路径
- `output`: 输出 PNG 路径
- `bg_hex`: 背景颜色,例如 `#000000`
- `threshold`: 颜色容差
- `alpha_threshold`: 已经接近透明的像素阈值
输出 `RemoveBgOutput`:
- `output_path`: 输出路径
- `removed_pixels`: 被清成透明的像素数
这个函数的行为不是“删除所有黑色像素”,而是:
- 从图片边界开始找背景
- 只删除和边界连通的背景区域
这样更安全,能尽量保住角色内部描边、眼睛、武器暗部这些黑色细节。
## 公开类型
对外暴露的主要类型有:
- `SliceOptions`
- `DetectOptions`
- `GroupOptions`
- `GifOptions`
- `RemoveBgOptions`
- `NormalizeOptions`
- `SliceOutput`
- `DetectOutput`
- `GroupedActionSummary`
- `GroupOutputSummary`
- `GifOutput`
- `RemoveBgOutput`
- `NormalizeOutput`
- `SliceManifest`
- `FrameRecord`
- `DetectionMode`
- `ActionSpec`
- `AnchorX`
- `AnchorY`
### `AnchorX`
支持:
- `AnchorX::Left`
- `AnchorX::Center`
- `AnchorX::Right`
字符串解析也支持:
- `"left"`
- `"center"`
- `"right"`
### `AnchorY`
支持:
- `AnchorY::Top`
- `AnchorY::Center`
- `AnchorY::Bottom`
字符串解析也支持:
- `"top"`
- `"center"`
- `"bottom"`
## Manifest 格式
`slice_sheet` 和 `detect_frames` 生成的 manifest 都是 `SliceManifest`。
关键字段:
- `source`: 原图路径
- `frame_width`
- `frame_height`
- `columns`
- `rows`
- `offset_x`
- `offset_y`
- `gap_x`
- `gap_y`
- `alpha_threshold`
- `min_opaque_pixels`
- `bg_hex`
- `bg_threshold`
- `detection`: `grid` 或 `connected_components`
- `frames`: 帧列表
`frames` 中每项是 `FrameRecord`:
- `index`: 帧索引
- `row`: 所在行
- `column`: 所在列
- `x`: 原图裁剪起点 x
- `y`: 原图裁剪起点 y
- `width`
- `height`
- `opaque_pixels`
- `kept`: 是否保留导出
- `file`: 导出的相对文件路径,可为空
示例:
```toml
source = "/abs/path/sheet.png"
frame_width = 64
frame_height = 64
columns = 4
rows = 2
offset_x = 0
offset_y = 0
gap_x = 0
gap_y = 0
alpha_threshold = 0
min_opaque_pixels = 1
bg_threshold = 0
detection = "grid"
[[frames]]
index = 0
row = 0
column = 0
x = 0
y = 0
width = 64
height = 64
opaque_pixels = 3210
kept = true
file = "frames/frame_0000_r00_c00.png"
```
## CLI 用法
### 按网格切图
```bash
sprite-slicer slice \
--input /path/to/sheet.png \
--output ./out/slice \
--frame-width 64 \
--frame-height 64 \
--columns 8 \
--rows 6 \
--skip-empty
```
### 从透明图自动检测单帧
```bash
sprite-slicer detect \
--input /path/to/sheet-transparent.png \
--output ./out/detect \
--min-opaque-pixels 5000 \
--padding 4 \
--row-tolerance 28
```
### 按动作分组
```bash
sprite-slicer group \
--manifest ./out/slice/frames.toml \
--config ./examples/actions.toml \
--output ./out/actions
```
### 导出 GIF 预览
```bash
sprite-slicer gif \
--input ./out/actions/idle \
--output ./out/previews/idle.gif \
--fps 8 \
--pad 4
```
### 去除黑色背景
```bash
sprite-slicer remove-bg \
--input ./sheet-black.png \
--output ./sheet-transparent.png \
--bg-hex "#000000" \
--threshold 8
```
### 统一帧尺寸和锚点
```bash
sprite-slicer normalize \
--input ./out/actions/idle \
--output ./out/normalized/idle \
--anchor-x center \
--anchor-y bottom \
--pad 4
```
## 推荐工作流
### 工作流 1:AI 生成黑底整图
1. `remove_background`
2. `detect_frames`
3. `group_actions`
4. `normalize_frames`
5. `export_gif`
### 工作流 2:规则像素角色表
1. `slice_sheet`
2. `group_actions`
3. `normalize_frames`
4. `export_gif`
### 工作流 3:导入游戏前整理
1. 对每个动作目录调用 `normalize_frames`
2. 把归一化后的 PNG 导入游戏引擎
3. 预览时用 `export_gif` 做检查
## 游戏导入建议
这个库本身不绑定具体引擎,但整理后的结果适合导入:
- Flutter Flame 2D
- Godot
- Cocos Creator
- Unity 2D
- 自己写的 sprite animation 系统
常见做法:
- 一个动作一个目录,如 `idle/`、`walk/`、`attack/`
- 每帧文件名按 `0000.png`、`0001.png` 递增
- 游戏里按目录读取并排序
- 锚点统一后,角色切动作时位置更稳定
## 依赖
- `image`
- `gif`
- `serde`
- `toml`
- `clap`
- `anyhow`
## 发布状态
当前 crate 已经是 `lib + bin` 结构,并且已通过:
```bash
cargo test
cargo check
cargo package --allow-dirty
```
发布到 crates.io:
```bash
cargo publish --registry crates-io
```