feat: 從找圖開始優化——高解析度官方設定稿 → 去背 → 合成為形象圖
1. 找圖(icon search)
從 Fandom API 撈角色頁的所有圖片,依「解析度 + 是不是官方設定稿」排序。
官方設定稿(Full Body / Character Design / Avatar)是最好的來源:
* 773×1056 起跳,遠勝角色資料庫的 230px 縮圖
* **多半本來就是透明底 PNG**,去背幾乎免費、邊緣完美
新增 icon measure:回報解析度、臉佔比、背景是透明/單色/有場景、去背難度。
2. 去背(icon cutout)→ icon/portrait-cutout.png
三條路徑自動選:
source-alpha 原圖已是透明底(官方設定稿常見)→ 完美
plain-background 純白/單色底,色距去背 + 最大連通區 + 補洞 → 很好
grabcut 有場景時用臉的位置當前景種子 → 普通
實測記錄:把臉從 2026 主視覺裁下來再 GrabCut,結衣的黑髮會被整片當成背景切掉;
換成官方設定稿之後這問題直接消失——所以「找對圖」比「去背演算法」更關鍵。
3. 合成(icon generate --from-cutout)
自動裁成頭肩構圖再疊到角色配色的漸層底上。官方設定稿常是正反兩面並排,
不裁會變成兩個人,所以 compose 預設 --crop head(--zoom 可調鬆緊)。
產出 icon.svg(內嵌同一張 PNG,自成一體不外連)、icon.png,
以及 icon/portrait.svg 與 512/1024 兩個解析度。
向量重繪(--features)保留為「找不到可用官方圖」時的退路。
已更新兩個真實人格(皆為 cutout 樣式,Wiki 同步已驗證):
ASUNA-01 Asuna's SAO Avatar Full Body(773×1056,透明底)— 對應 2026
《Unanswered//butterfly》重述的早期艾恩葛朗特
YUI-01 Yui's ALO Pixie Form Full Body(773×1056,透明底)— 現行 Unital Ring
章的導航妖精形態
順手修掉兩個 bug:
* 合成路徑下 svg 為 null,回傳時 Buffer.byteLength(null) 直接崩潰。
* verifyArea 解析 git status --porcelain 時,因為 git() 會 trim 輸出,
開頭空白已消失(" M x" → "M x"),正規式對不上,檔名前面多一個 M。
selftest 175 項全綠(新增第 ⑲ 節:找圖評分、去背三路徑、compose 預設裁頭肩、
去背圖在 icon/ 會同步、SVG 不外連、沒有去背圖時 --from-cutout 會擋下)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -714,6 +714,39 @@ check("柵格器有做 bounding box 裁剪(1024 才跑得動)", (() => {
|
||||
return Date.now() - t0 < 4000; // 沒有裁剪的話會慢好幾倍
|
||||
})());
|
||||
|
||||
console.log("⑲ 找圖 → 去背 → 合成");
|
||||
check("找圖:官方設定稿加權高於一般截圖", (() => {
|
||||
// wikiImageCandidates 的評分:解析度取 log2,命中 Full Body/Character Design 再加 8
|
||||
const big = { title: "File:Scene.png", width: 1920, height: 1080 };
|
||||
const sheet = { title: "File:Yui's ALO Pixie Form Full Body.png", width: 773, height: 1056 };
|
||||
const score = (r) => Math.round(Math.log2(r.width * r.height) * 10) / 10 +
|
||||
(/full.?body|character.?design|concept|profile|settei|avatar/i.test(r.title) ? 8 : 0);
|
||||
return score(sheet) > score(big);
|
||||
})());
|
||||
check("cutout 的三條路徑都有實作", (() => {
|
||||
const py = fs.readFileSync(path.join(HERE, "portrait.py"), "utf8");
|
||||
return py.includes("source-alpha") && py.includes("plain-background") && py.includes("grabcut");
|
||||
})());
|
||||
check("portrait.py 有 measure/faces/headshot/cutout/compose 五個模式", (() => {
|
||||
const py = fs.readFileSync(path.join(HERE, "portrait.py"), "utf8");
|
||||
return ["measure", "faces", "headshot", "cutout", "compose"].every((m) => py.includes(`"${m}"`));
|
||||
})());
|
||||
check("compose 預設裁頭肩(官方設定稿常是正反兩面,整張會變兩個人)", (() => {
|
||||
const py = fs.readFileSync(path.join(HERE, "portrait.py"), "utf8");
|
||||
return py.includes('args.crop == "head"') && py.includes("head_box");
|
||||
})());
|
||||
check("去背圖路徑在 icon/(Wiki 區涵蓋)",
|
||||
ic.CUTOUT_PNG === "icon/portrait-cutout.png" && covered("icon/portrait-cutout.png")[0] === "wiki");
|
||||
check("cutout 樣式的 SVG 內嵌同一張 PNG(自成一體、不外連)", (() => {
|
||||
const spec = ic.iconSpec("GAMMA-01", { palette: ic.parsePalette("hair=#aaa,accent=#333") });
|
||||
const svg = ic.wrapPngSvg(spec, Buffer.from([0x89, 0x50, 0x4e, 0x47]), 64);
|
||||
return svg.includes("data:image/png;base64,") && !svg.includes("http://example") &&
|
||||
svg.includes("<image");
|
||||
})());
|
||||
check("沒有去背圖時 --from-cutout 會擋下並指引流程",
|
||||
cli(["icon", "generate", "--session", S_CODE, "--force", "--from-cutout"],
|
||||
{ expectOk: false }).status !== 0);
|
||||
|
||||
console.log(`\n${"=".repeat(60)}\n通過 ${passed} 項,失敗 ${failed} 項 → ${failed === 0 ? "全部通過 ✅" : "有測試失敗 ❌"}`);
|
||||
console.log(`(暫存倉庫留在 ${STORE},可自行刪除)`);
|
||||
process.exit(failed ? 1 : 0);
|
||||
|
||||
Reference in New Issue
Block a user