こんにちは、パレイド思想部です。
前回はマスク生成の仕組みを解説しました。
今回は、生成したスプライト群を いわゆる「PNGTuber」として、3レイヤーに分解する仕組みです。body / head / face に分離することで、頭の揺れや表情切替をリアルタイムでアニメーションできるようになります。
本記事はローカル LLM による自動執筆パイプラインで生成されました。現段階ではクラウド AI(Claude 等)の補助や人間の編集が介在していますが、pareido.jp では最終的に AI が自律的にコンテンツを制作できる仕組みの構築を目指しています。
「絵を描く」から「データを設計する」へ
ここまでの工程で、表情差分付きのスプライト群は完成しています。しかし、表情差分を並べて切り替えるだけでは「パラパラ漫画」でしかありません。頭が微妙に揺れたり、体は静止したまま表情だけ変わったり——そうした自然な動きを出すには、画像を微妙に動かす表現が有効です。
これは絵を描く作業とは全く異なるスキルです。画像編集ソフトで手動でレイヤーを切り出し、アルファチャンネルを調整し、首の境界を処理し… という作業は、画像生成 AI でアバターを作ろうとしている人にはなかなかの重荷です。
パイプラインのコードは、ベース画像とマスク情報から自動的にレイヤーを分解します。body / head / face の3層に分離し、首の重なり処理まで含めて、すぐに PNGTuber ツールに読み込めるデータを出力します。
なぜレイヤー分解が必要か
PNGTuber のアニメーションには大きく2つの方式があります。
- スプライト差し替え方式 — 表情ごとに1枚の画像を用意し、切り替える
- レイヤー合成方式 — body / head / face を分離し、独立して動かす
スプライト差し替えは実装が簡単ですが、体と頭が常に固定で動きがありません。レイヤー合成なら、頭を微妙に傾けたり、体と独立して表情だけ変えたりできます。
3レイヤーの定義
| レイヤー | 内容 | 動き |
|---|---|---|
| body | 首から下(胴体・肩) | 静的、またはゆるい揺れ |
| head | 頭部(髪・頭蓋・耳)、表情領域は透明 | 左右の傾き・微動 |
| face | 表情部分(目・鼻・口・眉) | 感情切替・瞬き・口パク |
3枚を重ねると元のスプライトが復元されます。face レイヤーだけを差し替えれば表情が変わり、head レイヤーを回転させれば頭が傾きます。

抽出アルゴリズム
レイヤー分解はマスクのアルファ演算で行います。
# body: ベース画像から顔領域を透明に
body = base_nobg.copy()
body_alpha = body[:, :, 3].astype(float)
body_alpha *= (1.0 - expression_mask / 255.0)
body[:, :, 3] = body_alpha.astype(np.uint8)
# face: 感情スプライトから体領域を透明に
face = emotion_sprite.copy()
face_alpha = face[:, :, 3].astype(float)
face_alpha *= (expression_mask / 255.0)
face[:, :, 3] = face_alpha.astype(np.uint8)
# head: 頭部から表情領域を除いた部分
head = base_nobg.copy()
head_alpha = head[:, :, 3].astype(float)
head_alpha *= (head_mask / 255.0) * (1.0 - expression_mask / 255.0)
head[:, :, 3] = head_alpha.astype(np.uint8)
ポイントは、expression_mask を「穴」として使うことです。body と head の expression_mask 領域は透明になり、その穴を face レイヤーが埋めます。
首の重なり処理
body と head の境界(首)で隙間ができる問題があります。頭を傾けると、本来隠れている首の付け根が見えてしまう。
対策として、body レイヤーの上端を20〜40ピクセル、首の境界より上に延長しています。通常は head レイヤーで隠れるため見えませんが、頭が傾いた際の隙間を埋めるバッファとして機能します。
# 首境界を検出し、body を上に延長
neck_y = detect_neck_boundary(head_mask)
body_extended = body.copy()
body_extended[neck_y - 40 : neck_y, :, 3] = base_nobg[neck_y - 40 : neck_y, :, 3]
ただしこの仕組みは、長髪や装飾具など、境界線の上下にわたる構造物と相性が良くありません。Live2Dなどでは頭髪を別レイヤーとして扱うことで回避していますが、後述のように今回は簡易的な方法をとっています。
再構成誤差の検証
レイヤー分解が正しく行えているかを定量的に検証します。body + head + face を重ね合わせた結果が、元のスプライトとどれだけ一致するかを計測します。
reconstructed = composite_layers(body, head, face)
mae = np.mean(np.abs(original.astype(float) - reconstructed.astype(float)))
psnr = 10 * np.log10(255**2 / np.mean((original - reconstructed) ** 2))
| 指標 | 良好 | 許容 | 要修正 |
|---|---|---|---|
| MAE | < 5 | < 10 | > 10 |
| PSNR | > 40dB | > 35dB | < 35dB |
MAE が10を超える場合は、マスクの境界やフェザリングに問題がある可能性が高いです。
髪レイヤーの分離(オプション)
さらに凝った表現をしたい場合、髪を独立したレイヤーとして分離できます。Segment Anything Model(SAM)を使って髪領域をセグメンテーションし、head から分離します。
body → head → hair → face
髪レイヤーを分離すると、首を傾ける動作や、表情の変化で前髪が変化してしまうケースにある程度対応ができます。ただし、SAM の精度に依存するため、安定性はやや落ちます。特に髪の端部など、背景と入り混じる領域の検出は限界があります。また、髪といっても一枚絵で変化がないため、長髪などの自然な表現は難しく、短めのヘアスタイルや帽子でカバーするのも一つの手です。
次回予告
次回は、生成したスプライト群の後処理(背景除去・アップスケール)と、APNG/WebP サムネイルの生成です。49枚のスプライトを最終成果物として仕上げるパイプラインを解説します。



