spritefusion-pixel-snapper — 完整教學

把 AI 生成的「不工整 pixel art」對齊到真正的整數 grid,並量化到限定色盤;單檔 870 行 Rust,可同時編譯成 CLI 與 WASM 模組。

1. 專案定位

1.1 一句話定位

Pixel Snapper 是 Sprite Fusion(線上 tilemap editor)團隊釋出的 AI pixel art 後處理工具。它接收一張 AI 生成、解析度通常為 1024×1024 的「看起來像 pixel art 但 pixel 邊界其實亂飄」的圖,輸出一張 pixel 大小一致、grid 對齊、色盤受限的乾淨 sprite。

1.2 解決什麼痛點

AI image model(GPT Image、Nano Banana、SD 系列)在生成 pixel art 時三個老問題:

  • pixel 大小不一致:肉眼看像 16×16 grid,實際上每個「格子」可能是 15、16、17 個 source pixel 寬窄。
  • grid 漂移:圖的左半邊 pixel 大小是 16,右半邊變成 15,整張圖沒有單一一致的 grid base。
  • 色盤亂跳:同一個物件的同一塊顏色,被 model 渲染出十幾個相近但不同的 RGB 值,導致縮放後出現雜色。

直接把這種圖丟進遊戲 engine 會看到鋸齒、模糊、scaling artifact。Pixel Snapper 解決方法是:偵測真正的 grid 週期 → snap 每個 pixel 到 grid cell → 對 cell 內顏色做 k-means 量化。

1.3 為什麼選這個工具

  • 單檔 Rust(870 行),無外部 service / 無雲端依賴,可離線跑。
  • 同時提供 native CLI 與 WASM build target(官方 web demo 即為 WASM 版)。
  • MIT license + Sprite Fusion 商業團隊背書(spritefusion.com 是付費 tilemap editor 廠商)。
  • 1 個檔案 = 1 個 binary,導入 game asset pipeline 阻力極低。

2. 安裝指南

2.1 系統需求

  • Rust toolchain(cargo),建議 ≥ 1.70。
  • 若要 WASM build:另裝 wasm-pack
  • 無 Python、無 node、無 GPU 需求。

2.2 CLI 安裝

1git clone https://github.com/Hugo-Dz/spritefusion-pixel-snapper.git
2cd spritefusion-pixel-snapper
3cargo build --release
4# binary 位置:target/release/spritefusion-pixel-snapper

或直接用 cargo run

1cargo run --release -- input.png output.png 16 --pixel-size 8

2.3 WASM 建置

1cargo install wasm-pack
2wasm-pack build --target web --out-dir pkg --release
3# 產出:pkg/spritefusion_pixel_snapper.js + .wasm

嵌入網頁:

1import init, { process_image } from "./pkg/spritefusion_pixel_snapper.js";
2await init();
3const outputBytes = process_image(inputBytes, 16, null);

第三參數為 pixel_size_override,傳 null 走自動偵測。

3. 核心架構解析

3.1 演算法 pipeline

整個 process_image_bytes_common()(src/main.rs L102)的流程:


flowchart TD
    A[Input PNG bytes] --> B[image.load_from_memory]
    B --> C[validate_image_dimensions]
    C --> D[quantize_image - k-means++ 色彩量化]
    D --> E[compute_profiles - 計算 X/Y 軸投影輪廓]
    E --> F[estimate_step_size - 偵測 grid 週期]
    F --> G{step 偵測成功?}
    G -->|皆失敗| H[fallback 用 fallback_target_segments]
    G -->|單軸失敗| I[用 sibling 軸 step]
    G -->|皆成功| J[直接使用]
    H --> K[walk - 1D walker 找切割線]
    I --> K
    J --> K
    K --> L[stabilize_both_axes 雙軸交叉校驗]
    L --> M[resample 重新取樣到 grid]
    M --> N[image.write_to PNG]
    N --> O[Output PNG bytes]

3.2 模組分工

函式行範圍職責
parse_args()L196–243CLI 參數解析(input / output / k-colors / --pixel-size
validate_image_dimensions()L262防呆:圖太小或非法尺寸
quantize_image()L276k-means++ 初始化 + Lloyd iteration,把所有 RGB 壓到 K 色
compute_profiles()L416沿 X 軸 / Y 軸算 luminance 差異累積,得到 1D 訊號
estimate_step_size()L458從 1D 訊號用 peak detection 估 grid 週期
resolve_step_sizes()L502雙軸 fallback 策略(單軸失敗用 sibling,雙軸失敗用 fallback)
walk()L6101D walker,從第一個 peak 開始按 step_size 跳,找所有切割線
stabilize_both_axes()L537雙軸交叉驗證(先 raw cuts 再 cross-validate)
stabilize_cuts()L658單軸切割線穩定化
snap_uniform_cuts()L736fallback:強制均勻切 N 段
resample()L817用 col_cuts / row_cuts 從原圖採樣,輸出 grid_w × grid_h 圖

3.3 Config 結構

 1pub struct Config {
 2    pub k_colors: usize,                    // 預設 16
 3    pub pixel_size_override: Option<f64>,   // None = 自動偵測
 4    k_seed: u64,                            // k-means++ 種子
 5    max_kmeans_iterations: usize,
 6    peak_threshold_multiplier: f64,
 7    peak_distance_filter: usize,
 8    walker_search_window_ratio: f64,
 9    walker_min_search_window: f64,
10    walker_strength_threshold: f64,
11    min_cuts_per_axis: usize,
12    fallback_target_segments: usize,
13    max_step_ratio: f64,
14    // + input_path / output_path(僅 CLI 用)
15}

所有閾值都集中在 Config,讓使用者只需暴露 k_colorspixel_size_override 兩個外部旋鈕。

4. CLI 詳細用法

4.1 最小命令

1cargo run --release -- input.png output.png
2# k_colors 預設 16
3# pixel_size 自動偵測

4.2 指定 k_colors

1cargo run --release -- input.png output.png 32
2# 第三個位置參數即 k_colors

4.3 覆寫 pixel size

1cargo run --release -- input.png output.png 16 --pixel-size 8
2# 強制把每 8×8 source pixel 視為一個 grid cell

--pixel-size 的合法範圍是 [1, min(width, height) / 2],傳超出範圍會回 InvalidInput

4.4 自動偵測失敗的訊號

執行時印出:

1Pixel size: 8.0px (auto-detected)
2Output size: 128x128

Pixel size: 8.0px (override) 表示走 fallback;若 stdout 顯示 grid 跟你預期差很多,加 --pixel-size N 強制覆寫。

5. 應用場景

5.1 AI 生成 sprite 後處理

最直接的場景:把 GPT Image / Nano Banana 生成的 1024×1024 「pixel art-ish」圖丟進去,輸出 64×64 或 128×128 乾淨 sprite,可直接進 Unity / Godot / GB Studio。

5.2 自動化 batch pipeline

process_image() 既是 WASM entry 也可在 Rust 內部直接呼叫。寫個外層 Rust binary 把整個資料夾的圖串成 batch:

1for entry in fs::read_dir("./sprites_in")? {
2    let bytes = fs::read(entry?.path())?;
3    let out = process_image_bytes_common(&bytes, None)?;
4    fs::write(out_path, out)?;
5}

5.3 整合到 web tool

Sprite Fusion 自家的線上 editor 就是把 WASM build 包進 React/Svelte 前端,使用者拖檔即可看到結果。任何想加「snap to grid」按鈕的 web 工具都可以複製這條路徑。

5.4 不適合的場景

  • 高解析度寫實風 AI 圖(不是 pixel art-ish),snap 完會變得「完全認不出」(issue #9 真實案例)。
  • 已經乾淨的 pixel art:snap 沒幫助,反而可能把細節抹掉。
  • 透明度有複雜 alpha gradient 的 sprite:演算法把 a == 0 視為背景跳過,半透明邊緣可能不穩。

6. 資安掃描報告

6.1 結論:🟢 低風險

針對全 repo(870 行 Rust + 32 行 .gitignore + Cargo.toml)做 grep -rnE "eval|exec|os.system|subprocess|shell=True|curl|wget|http|urlopen|requests|pickle|__import__|input\(|raw_input|secret|token|password|api_key|API_KEY|unsafe"僅 1 個 match

1src/main.rs:319:    // Maybe try a faster algorithm for this? like https://crates.io/crates/kmeans_colors

純註解中的參考連結,無實際網路呼叫、無 secret、無 shell exec、無 unsafe block。

6.2 風險紅黃綠燈

類別等級說明
任意命令執行(shell / exec)🟢 低完全無 Command::spawn / std::process / shell escape
網路請求🟢 低無 reqwest / hyper / tokio-net 依賴;純檔案 IO
機密外洩🟢 低無 API key / token / env var 讀取(除 env::args 取 CLI 參數)
unsafe Rust🟢 低全檔無 unsafe block;wasm-bindgen 暴露介面用標準 #[wasm_bindgen]
依賴鏈 supply chain🟢 低依賴僅 5 個(image / rand / rand_distr / rand_chacha / wasm-bindgen),皆為 crates.io 知名套件
路徑遍歷 / 任意讀寫🟡 中(CLI mode)CLI 直接用 env::args 接 input/output path 傳給 image::openimage::save;若整合到網頁後端,需自行做路徑白名單
Image parsing DoS🟡 中image::load_from_memory 接外部 bytes;超大圖 / malformed PNG 可能耗 RAM 或 panic(雖有 validate_image_dimensions 防呆,但未限制 max pixel count)
WASM 沙箱🟢 低全部運算在 wasm sandbox,無法逃逸到 host

6.3 整合到內部 pipeline 的建議

  • 本地 CLI 用:直接使用,無需額外加固。
  • 整合到 web service 後端:必須在外層加(a)檔案大小上限、(b)pixel 總數上限、(c)路徑白名單、(d)timeout。
  • 整合到瀏覽器前端(WASM):照官方 demo 即可;WASM sandbox 已隔離。

7. FAQ

Q1:為什麼我輸出的圖完全認不出原圖? A:你的輸入可能不是「pixel art-ish」而是寫實圖。Pixel Snapper 不是 pixelate filter,是 snap 工具。先用 prompt 引導 AI 生成「明顯像素風」圖再丟進來。

Q2:可以處理透明背景嗎? A:可以(PR #2 已支援)。但複雜 alpha gradient(半透明邊緣)可能不穩;演算法只看 a == 0 跳過,不對中間 alpha 做特別處理。

Q3:max k-colors 是多少? A:CLI 無上限(受記憶體限制);官方 web 版曾被反映「32 太少」,後來已上調。

Q4:為什麼自動偵測 grid 大小不準? A:通常是 source 圖太「乾淨」沒有明顯 luminance peak,或太「髒」雜訊壓過 peak。對策:用 --pixel-size N 手動指定。

Q5:可以做 batch / pipeline 嗎? A:可以。process_image_bytes_common() 是純 in-memory 函式,外層自己包 batch loop。或寫個 shell for f in *.png; do cargo run ... -- $f out/$f; done

Q6:WASM build 多大? A:release build 約 ~200KB gzipped(image crate + rand 系列 + bindgen overhead)。

8. 進階技巧

8.1 調整 Config 內部閾值

Config 內 10+ 個非公開欄位(max_kmeans_iterations / peak_threshold_multiplier / …)控制演算法精度。要試不同設定需 fork 改 Default::default()

建議優先調的兩個:

  • max_kmeans_iterations:預設 10。圖太花需提高到 20–30 換更穩定色盤。
  • peak_threshold_multiplier:grid 偵測敏感度,調低可偵測更細的 grid。

8.2 用 k_seed 控制可重現性

k_seed 固定種子 → 同樣 input 永遠得到同樣 output。對 CI / regression test 很重要。

8.3 投影輪廓 debug

compute_profiles() 回傳 (profile_x, profile_y) 兩個 Vec<f64>。fork 一份把它 dump 成 CSV,畫圖即可直觀看出 grid 偵測在哪裡失敗。

9. 整合進其他工作流

9.1 與本專案的 layer 關係

場景建議走法
拿到一張 AI sprite 想清理本工具(CLI mode)
想做 AI sprite batch 工作流本工具 + 自寫 Rust outer batch
想做 web demo本工具 WASM build + 前端
想存進 inbox/ 當參考本 tutorial 已存;後續案例用 ai-gh-save / gh-tutorial-qd 收錄變體實作

9.2 與 Sprite Fusion 生態

Sprite Fusion 還有:tilemap editor(付費 web app)、各種 engine exporter(Unity / Godot / Defold / GB Studio)。完整 sprite pipeline 是:「AI 生圖 → Pixel Snapper(本工具)→ Sprite Fusion editor 切片排版 → engine import」。

10. 重點摘要 Checklist

  • 870 行單檔 Rust,演算法乾淨可讀
  • 同時支援 CLI 與 WASM
  • MIT license,可商用
  • 資安掃描 🟢 低風險(無網路、無 shell、無 unsafe)
  • 已知不適用:寫實風 AI 圖、複雜 alpha gradient
  • 主要旋鈕只有兩個:k_colors--pixel-size
  • 自動偵測失敗時有雙軸 fallback + 強制均勻 fallback
  • 與 Sprite Fusion 編輯器生態整合度高(同團隊出品)

11. 進一步閱讀