sprite-slicer 0.1.2

Sprite-sheet slicing, transparent sprite detection, action grouping, background removal, frame normalization, and GIF preview export.
Documentation

sprite-slicer

sprite-slicer 是一个面向 2D 游戏素材流程的 Rust crate,既能作为命令行工具使用,也能作为库嵌入到你自己的工具链中。

它解决的是一条完整的 sprite 处理链路:

  • 把规则 sprite sheet 按网格切成单帧
  • 从透明背景图中自动检测单帧
  • 按动作把帧重新分组
  • 去除黑色背景并转透明 PNG
  • 把一组帧统一到同一画布和锚点
  • 导出 GIF 预览动画

仓库地址:

适用场景

  • AI 生成了一整张角色动作图,需要切成单帧
  • 一张图里角色站位不规则,需要自动检测角色块
  • 想把帧按 idle / walk / attack / hurt 分到不同目录
  • 想把黑底或近黑底图片转成透明背景 PNG
  • 想把动作帧对齐到统一尺寸,避免游戏里角色抖动
  • 想快速导出 GIF 看动画节奏是否正常

特性

  • 同时提供 CLIlibrary API
  • 输入输出都是文件和目录,适合脚本、工具链、AI agent 调用
  • 支持规则网格切图和透明图自动检测
  • 支持动作分组导出
  • 支持背景去除和帧规范化
  • 支持 GIF 预览导出
  • 适合像素角色、敌人动画、JRPG sprite workflow

安装

作为库:

cargo add sprite-slicer

作为命令行工具:

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_framesexport_gif 只接受 PNG 文件或只包含 PNG 的目录
  • 对 AI 生成图优先尝试 detect_frames,对规则表格图优先尝试 slice_sheet

AI 调用模板

下面这段可以直接复制给别的 AI,当作它操作这个库的规则。

你正在操作一个 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>

可直接引入:

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. 黑底转透明

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. 规则网格切图

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. 透明图自动检测

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. 按动作分组

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

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(())
}
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 预览

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

签名:

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

签名:

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

签名:

pub fn group_actions(options: GroupOptions) -> anyhow::Result<GroupOutputSummary>

用途:

  • 根据动作配置,把切好的帧复制到各自动作目录
  • 适合把一堆 frames/*.png 重新组织成 idle/ walk/ attack/

GroupOptions 字段说明:

  • manifest: slice_sheetdetect_frames 产出的 frames.toml
  • config: 动作配置 TOML 文件
  • output: 动作输出目录

动作配置格式:

[[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

签名:

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

签名:

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

签名:

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_sheetdetect_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: gridconnected_components
  • frames: 帧列表

frames 中每项是 FrameRecord

  • index: 帧索引
  • row: 所在行
  • column: 所在列
  • x: 原图裁剪起点 x
  • y: 原图裁剪起点 y
  • width
  • height
  • opaque_pixels
  • kept: 是否保留导出
  • file: 导出的相对文件路径,可为空

示例:

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 用法

按网格切图

sprite-slicer slice \
  --input /path/to/sheet.png \
  --output ./out/slice \
  --frame-width 64 \
  --frame-height 64 \
  --columns 8 \
  --rows 6 \
  --skip-empty

从透明图自动检测单帧

sprite-slicer detect \
  --input /path/to/sheet-transparent.png \
  --output ./out/detect \
  --min-opaque-pixels 5000 \
  --padding 4 \
  --row-tolerance 28

按动作分组

sprite-slicer group \
  --manifest ./out/slice/frames.toml \
  --config ./examples/actions.toml \
  --output ./out/actions

导出 GIF 预览

sprite-slicer gif \
  --input ./out/actions/idle \
  --output ./out/previews/idle.gif \
  --fps 8 \
  --pad 4

去除黑色背景

sprite-slicer remove-bg \
  --input ./sheet-black.png \
  --output ./sheet-transparent.png \
  --bg-hex "#000000" \
  --threshold 8

统一帧尺寸和锚点

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.png0001.png 递增
  • 游戏里按目录读取并排序
  • 锚点统一后,角色切动作时位置更稳定

依赖

  • image
  • gif
  • serde
  • toml
  • clap
  • anyhow

发布状态

当前 crate 已经是 lib + bin 结构,并且已通过:

cargo test
cargo check
cargo package --allow-dirty

发布到 crates.io:

cargo publish --registry crates-io