# LLMで生成したイラストを小さなSVGに変換する実験

Markdownで書く個人ブログに、ちょっとしたイラストを埋め込みたくなった。

自作のブログエンジンは、意図的に制限したMarkdownのサブセットをHTMLへ変換するだけの簡単なものにしている。装飾は基本的にPico CSSに任せ、画像の外部参照やクライアント側JavaScriptもなるべく使わない方針だ。

ただし、コードブロックの言語に `svg` を指定した場合は、その内容をSVG画像として描画できるようにしてある。

例えば、順序関係や処理の流れなら、こんな図を直接埋め込める。

```svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 100">
  <g fill="none" stroke="currentColor" stroke-width="1.5">
    <rect x="10" y="25" width="115" height="50" rx="8"/>
    <rect x="183" y="25" width="115" height="50" rx="8"/>
    <rect x="356" y="25" width="115" height="50" rx="8"/>
    <path d="M125 50H175m-7 -6 7 6-7 6M298 50H348m-7 -6 7 6-7 6"/>
  </g>
  <g fill="currentColor" font-size="15" text-anchor="middle">
    <text x="67" y="55">入力</text>
    <text x="240" y="55">変換</text>
    <text x="413" y="55">出力</text>
  </g>
</svg>
```

この程度の図なら、LLMにSVGを書かせても十分実用的だ。順序関係や構成要素の関係性を説明する図は、そもそも矩形や線、矢印などの組み合わせで表現できる。

では、動物のイラストのようなものはどうか。

今回はタヌキのイラストを例に、画像生成モデルが作った画像を、小さなSVGへ変換する実験をした。

## 目標

もともとは、日本人をタヌキに例えるなら、韓国人や中国人はどんな動物に対応させると面白いか、という雑談から始まった。

タヌキと対になるキツネ、タヌキと間違えられやすいアライグマ、といった連想だ。

せっかくなので、この3種類の動物を並べたイラストを作りたくなった。ただし、今回試したのはタヌキだけ。

目標は次のようなもの。

- Markdownの `svg` コードブロックに直接埋め込める。
- 外部画像やスクリプトには依存しない。
- 数KB程度の小さなSVGにする。
- 200〜300px程度で表示したときに、ちゃんとかわいく見える。
- 将来、同じスタイルでキツネやアライグマも描ける。

SVGを使う以上、拡大しても綺麗なのは当然うれしい。ただ、今回は小さなイラストなので、細部を完全に再現する必要はない。

## LLMにSVGを直接書かせてみた

まず、LLMにタヌキを直接SVGで描いてもらった。

これはあまりうまくいかなかった。

丸や楕円を並べたタヌキらしい何かにはなるものの、画像生成モデルが出力するような、自然でかわいいキャラクターにはならない。

一方、画像生成モデルで作ったタヌキはかなりよかった。

ここで感じたのは、LLMがSVGを直接生成する場合、画像全体の造形を保ちながら個々の座標を決める必要があるという難しさだ。

もちろん、LLMによるSVG生成が常に悪いわけではない。構造図などでは十分使えるし、描画結果を見ながら修正することで改善もできる。

ただ、今回は最初から画像生成モデルを利用した方が手軽そうだった。

そこで、次の2つの方向を考えた。

1. 画像生成モデルで作った画像を、ツールでSVGへ変換する。
2. SVG描画ライブラリなどで座標の扱いを抽象化し、レンダリング結果を見ながらLLMに修正させる。

どちらの場合も、重要なのは一発で完成させようとしないことだと思う。

## まずは元画像をデフォルメする

最初に作ったタヌキは、毛並みや陰影まで描かれた、かなり凝ったイラストだった。

見た目はよかったが、これをそのままSVGへ変換すると細かなパスが大量に生成されそうだ。

そこで、最初からSVG化しやすい画像を生成することにした。

今回は次のような特徴を持つタヌキを選んだ。

- 二足歩行のマスコット体型
- 丸い顔と胴体
- 大きなクリーム色のお腹
- 目の周りの濃い茶色の模様
- 小さな耳と短い手足
- 大きいが、アライグマほど縞模様の強くない尻尾

毛並みを細かく描くのではなく、少数の色面でキャラクターを表現する方向だ。

この段階で、元画像のかわいらしさを保ちながら、かなり単純な形状になった。

### 画像生成用のプロンプト

今回気に入った二足歩行タヌキのスタイルを再現するため、次のプロンプトを記録しておく。

> Create a cute, simplified, chibi-style tanuki mascot illustration: upright bipedal pose, front-facing or slightly front-facing, very rounded and plump silhouette, flat vector-like shapes, minimal detail, clean large color regions, transparent background. The tanuki should have a warm brown color palette, a large cream-colored belly and muzzle, dark brown eye-mask markings, small rounded ears, short legs, small rounded paws, and a large fluffy tail with soft darker banding but not strongly striped like a raccoon. Make it adorable, simple, and mascot-like, with a cheerful open-mouth smile and shiny round eyes. Keep the design visually distinct from a fox and a raccoon by emphasizing the tanuki's round body, softer face, and gentler tail pattern. Avoid realistic fur texture, avoid tiny details, avoid background elements, and make it suitable for later SVG conversion into a compact markdown-embeddable SVG.

厳密な再現性が必要なら、プロンプトだけではなく、元画像も参照画像として保持しておいた方がよい。

## SVGへの変換

本来は [VTracer](https://github.com/visioncortex/vtracer) を試したかった。

VTracerはラスター画像をSVGに変換するツールで、色の領域や輪郭を抽出し、曲線へ近似する処理を備えている。

ただ、今回の実行環境にはインストールされておらず、ネットワーク制限で追加インストールもできなかった。

そのため、今回はOpenCVを使った簡易的な変換処理を組み立てた。

```svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 330">
  <g font-family="sans-serif" font-size="14" text-anchor="middle" fill="currentColor">
    <g fill="none" stroke="currentColor" stroke-width="1.5">
      <rect x="140" y="10" width="200" height="44" rx="7"/>
      <rect x="140" y="78" width="200" height="44" rx="7"/>
      <rect x="140" y="146" width="200" height="44" rx="7"/>
      <rect x="140" y="214" width="200" height="44" rx="7"/>
      <rect x="140" y="282" width="200" height="44" rx="7"/>
      <path d="M240 54V70m-6 -6 6 6 6 -6M240 122V138m-6 -6 6 6 6 -6M240 190V206m-6 -6 6 6 6 -6M240 258V274m-6 -6 6 6 6 -6"/>
    </g>
    <text x="240" y="37">元画像を320pxに縮小</text>
    <text x="240" y="105">K-meansで色数を削減</text>
    <text x="240" y="173">色ごとに輪郭を抽出</text>
    <text x="240" y="241">輪郭を単純化してSVG化</text>
    <text x="240" y="309">レンダリングして比較</text>
  </g>
</svg>
```

処理は単純で、OpenCVのK-meansで色数を減らし、色ごとにマスクを生成して輪郭を抽出する。

得られた輪郭を `approxPolyDP` で単純化し、SVGの `path` 要素として出力した。

画像を忠実に再現するというより、色分けされた画像を少数の輪郭で表現する方法だ。

### どの程度小さくできたか

6色、10色、14色の3種類について、輪郭の単純化の強さも変えながら実験した。

| 色数  | 輪郭の単純化 | SVGサイズ |
| --- | ------ | ------ |
| 6色  | 弱め     | 5.5 KB |
| 6色  | 強め     | 4.4 KB |
| 10色 | 弱め     | 6.1 KB |
| 10色 | 強め     | 4.9 KB |
| 14色 | 弱め     | 9.9 KB |
| 14色 | 強め     | 7.4 KB |

数KBという目標は達成できた。

ただし、見た目には問題があった。

特に、輪郭が角張ってしまうこと、目や口元が崩れること、色面の境界に隙間が生じることが目立った。

色数を増やせば改善するかと思ったが、必ずしもそうではなかった。色数を増やすと、元画像の細かな色の違いまで領域として抽出してしまい、かえってノイズが増える。

一度、画像のシルエットを下地として敷いてから各色の領域を重ねるようにすると、色面の境界の隙間はある程度改善した。

しかし、顔の崩れは残った。

## ベジェ曲線にすればよくなるのか

次に考えたのは、輪郭を滑らかなベジェ曲線にする方法だ。

`approxPolyDP` は、輪郭を基本的に直線の集合として近似する。

SVGならベジェ曲線を使えるので、滑らかな輪郭を少数の制御点で表現できる可能性がある。

そこで、先ほどの10色・約4.9KBのSVGを基準に、曲線への近似を試してみた。

結果は次の通り。

| 方法      | SVGサイズ  |
| ------- | ------- |
| 従来の輪郭近似 | 約4.9 KB |
| 曲線化 0.4 | 15.6 KB |
| 曲線化 0.7 | 15.7 KB |
| 曲線化 1.0 | 12.3 KB |

見た目は少し滑らかになった。

しかし、劇的な改善はなかった。サイズはむしろ2〜3倍に増えてしまった。

理由を考えると、曲線化は元の輪郭を滑らかにするだけであり、元画像から抽出された形状がそもそもおかしければ、その問題を解決できない。

例えば、目の模様が不自然な形で抽出されてしまった場合、その輪郭を滑らかな曲線にしても、かわいい目の模様にはならない。

つまり、問題は単なる曲線近似ではなく、どの形状を残すべきかという判断にありそうだ。

## 顔だけ描き直してみる

そこで、もう少し違う方法を試した。

胴体や尻尾については自動トレースの結果を維持し、顔の重要な部分だけをSVGの図形として作り直す。

具体的には、次の部分を再構成した。

- 左右の目の模様
- 瞳とハイライト
- 鼻と口元
- 口の曲線

こうした特徴は、複雑な輪郭を大量に使うよりも、少数の楕円やベジェ曲線で表現した方が扱いやすい。

結果は約6.2KBになった。

元の4.9KBよりは少し大きくなったが、顔の認識しやすさは明確に改善した。

一方、胴体や耳、手足にはまだ粗さが残っている。

完全に満足できる結果ではないものの、全体を機械的に曲線化するよりは改善の方向がはっきりしていた。

## 今回分かったこと

今回の実験で、最も印象に残ったのは次の点だ。

### 画像の細部を再現することと、よいイラストを作ることは違う

自動トレースは、元画像の色や輪郭をできるだけ保存しようとする。

しかし、イラストとして重要な特徴は均等に分布していない。

タヌキなら目の模様、鼻、丸い顔、体型、尻尾などが特に重要だ。これらが崩れると、ほかの部分を細かく再現しても、全体の印象は悪くなる。

逆に、毛並みの細かい陰影などは、省略してもキャラクターの印象を保てる場合がある。

この違いをうまく利用できれば、小さなSVGでも十分に高品質なイラストが作れそうだ。

### SVGのサイズと見た目は必ずしも比例しない

今回、4.9KBのSVGを曲線化したところ、12〜16KBになっても大幅な品質向上は得られなかった。

一方、顔だけ再構成した6.2KB版では、比較的小さなサイズ増加で見た目が改善した。

つまり、単にパスの数や制御点を増やすのではなく、何を表現するために使うかが重要らしい。

### LLMに一発描きさせる必要はない

最初のSVG生成では、LLMにコードを書かせ、その結果をほとんど確認せずに出力していた。

今回のように実行環境を使えるなら、わざわざ一発で完成させる必要はない。

画像生成、変換、レンダリング、比較、修正というループを回せばよい。

```svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 245">
  <g font-family="sans-serif" font-size="14" text-anchor="middle" fill="currentColor">
    <g stroke="currentColor" stroke-width="1.5" fill="none">
      <rect x="10" y="25" width="120" height="50" rx="8"/>
      <rect x="180" y="25" width="120" height="50" rx="8"/>
      <rect x="350" y="25" width="120" height="50" rx="8"/>
      <path d="M130 50H172m-7 -6 7 6-7 6M300 50H342m-7 -6 7 6-7 6"/>
      <rect x="180" y="155" width="120" height="50" rx="8"/>
      <path d="M410 75V180H308m7 -6-7 6 7 6"/>
      <path d="M180 180H70V83m-6 7 6 -7 6 7" stroke-dasharray="5 4"/>
    </g>
    <text x="70" y="55">画像生成</text>
    <text x="240" y="55">SVG変換</text>
    <text x="410" y="55">レンダリング</text>
    <text x="240" y="185">視覚的な評価・修正</text>
    <text x="125" y="172" font-size="11">再試行</text>
  </g>
</svg>
```

ただし、レンダリングした画像を見せるだけで自動的に改善できるわけではない。

どの部分が悪いかを特定し、その部分に対して有効な変更を行う必要がある。

今回の場合は、顔の形状を個別に再構成するという変更が、それにあたる。

## 次に試したいこと

今回のOpenCVによる簡易トレースは、あくまで代替手段だった。

次は本来使いたかったVTracerを試してみたい。

VTracerでは曲線近似を含むベクター化ができるため、今回の簡易実装より良い結果が得られる可能性がある。ただし、ファイルサイズと品質の両方で優れているかは、実際に比較しないとわからない。

また、画像から意味的なSVG要素を生成する [StarVector](https://github.com/joanrod/star-vector) のような専用モデルも興味深い。

もう一つ考えられるのは、画像全体をトレースするのではなく、LLMに意味のある部品へ分解させて、それぞれを描画ライブラリで構築する方法だ。

最初から胴体、顔、耳、目、尻尾という構造を持たせれば、変更もしやすくなる。

ただし、座標を抽象化したからといって造形の問題がなくなるわけではないので、やはり視覚的な確認は必要になるだろう。

## まとめ

今回のタヌキのSVG化では、数KBまで小さくすることには成功したものの、元画像のかわいらしさを十分に再現するところまでは到達しなかった。

一方で、部分的に描き直すことで、ファイルサイズをそれほど増やさずに改善できることも確認できた。

現時点では、次のような役割分担がよさそうだ。

- 画像生成モデルに、キャラクターの造形を任せる。
- 変換ツールに、基本的な色面や輪郭の抽出を任せる。
- LLMには、レンダリング結果の評価と、重要な部分の修正を任せる。

最終的には、自動トレースの結果をそのまま採用するのではなく、意味のある少数の図形に整理する処理がほしい。

目指しているのは、完璧なベクター化ではない。

**Markdownの中に気軽に貼れる程度の短いSVGで、ちゃんとかわいいタヌキを表示できること。**

今のところ、そのためには画像を一度生成し、見た目を確認しながら少しずつSVGを組み立て直すのがよさそうだ。

ちなみに現在のタヌキの絵はこのようなものだ。これはベジェ曲線などを使う前のもの。

```svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 320">
<path fill="#96603f" d="M94 12 L81 22 L75 32 L73 39 L73 52 L75 57 L56 79 L41 89 L42 94 L39 97 L40 110 L48 124 L58 133 L43 149 L36 160 L31 173 L31 186 L35 194 L29 214 L29 236 L32 248 L43 269 L59 284 L51 293 L52 299 L57 303 L62 303 L69 307 L88 306 L97 301 L104 292 L105 288 L112 282 L144 281 L153 285 L167 288 L172 298 L181 306 L188 308 L208 307 L220 299 L220 289 L216 279 L228 267 L243 267 L260 262 L281 249 L295 234 L301 225 L311 202 L313 192 L313 170 L310 160 L305 152 L296 145 L283 141 L272 141 L257 145 L245 152 L233 164 L226 174 L219 161 L212 153 L223 146 L233 136 L236 130 L235 125 L237 123 L236 119 L228 108 L218 80 L224 73 L228 61 L227 46 L222 35 L217 30 L209 26 L197 26 L191 28 L177 39 L156 32 L132 32 L126 21 L117 13 L108 10Z"/>
<path fill="#995c3b" fill-rule="evenodd" stroke="#995c3b" stroke-width=".7" stroke-linejoin="round" d="M176 38 L148 31 L115 36 L82 52 L41 90 L40 110 L57 134 L43 149 L32 169 L31 186 L34 197 L29 214 L29 236 L39 263 L58 283 L85 287 L110 284 L75 272 L55 256 L43 235 L39 206 L41 201 L52 206 L50 196 L58 180 L72 170 L84 168 L77 157 L90 142 L101 136 L116 133 L108 125 L105 111 L89 116 L80 115 L68 106 L66 100 L68 88 L74 79 L90 65 L108 57 L126 60 L135 73 L134 89 L150 92 L155 78 L161 71 L171 66 L185 68 L204 85 L214 108 L211 124 L201 132 L185 131 L169 120 L162 132 L149 137 L166 147 L177 164 L167 171 L178 176 L189 190 L190 203 L183 213 L202 209 L204 232 L197 250 L184 265 L164 277 L147 283 L166 288 L191 285 L217 278 L227 269 L235 255 L239 240 L239 218 L231 182 L242 183 L256 189 L272 205 L280 226 L276 252 L291 239 L300 226 L300 210 L296 195 L279 172 L263 164 L243 163 L232 167 L226 173 L212 153 L233 136 L237 121 L228 108 L222 88 L210 66 L196 51Z"/>
<path fill="#f3d6b5" fill-rule="evenodd" stroke="#f3d6b5" stroke-width=".7" stroke-linejoin="round" d="M181 95 L179 99 L184 102 L187 97Z M144 91 L131 91 L117 97 L108 108 L107 119 L121 134 L106 136 L92 142 L79 156 L80 161 L100 179 L103 186 L101 196 L88 207 L69 211 L42 204 L42 225 L54 253 L74 270 L94 278 L113 282 L142 282 L160 277 L181 266 L193 254 L201 238 L201 210 L184 215 L153 210 L139 193 L141 184 L148 177 L175 165 L174 159 L164 147 L145 138 L165 127 L167 112 L158 98Z M135 96 L144 97 L149 101 L148 105 L141 109 L144 113 L154 114 L139 128 L130 126 L123 112 L124 108 L129 111 L136 108 L131 103Z M108 81 L105 87 L110 89 L112 83Z M196 43 L193 47 L211 65 L215 59 L213 45 L207 41Z M111 31 L102 25 L95 26 L87 35 L87 46 L112 36Z"/>
<path fill="#532e22" fill-rule="evenodd" stroke="#532e22" stroke-width=".7" stroke-linejoin="round" d="M51 297 L55 302 L69 307 L83 307 L97 301 L105 289 L59 284 L52 291Z M220 299 L220 289 L215 279 L194 286 L168 289 L178 304 L188 308 L208 307Z M143 184 L141 193 L144 198 L153 193 L149 199 L152 207 L172 213 L179 213 L186 208 L189 200 L187 189 L174 175 L164 172 L149 178Z M100 182 L88 170 L76 170 L57 183 L52 193 L52 201 L55 206 L64 209 L78 206 L77 200 L81 206 L86 206 L92 201 L91 193 L95 198 L99 196Z M233 164 L256 161 L268 164 L282 173 L298 196 L303 221 L313 192 L313 170 L308 156 L302 149 L283 141 L266 142 L251 148Z M135 113 L129 114 L128 119Z M163 71 L155 81 L153 92 L166 105 L170 119 L190 131 L199 131 L207 127 L212 119 L212 106 L201 83 L181 68Z M177 88 L185 89 L192 96 L194 108 L188 116 L175 115 L168 105 L170 94Z M131 67 L125 61 L115 58 L94 64 L70 87 L69 105 L82 114 L103 111 L117 95 L133 87 L134 76Z M110 75 L118 85 L116 95 L105 104 L97 104 L89 96 L90 87 L98 78Z M184 32 L179 39 L190 45 L201 39 L207 39 L215 45 L217 59 L212 68 L220 78 L228 61 L227 46 L222 35 L209 26 L197 26Z M128 24 L120 15 L111 11 L101 10 L87 16 L75 32 L74 55 L85 49 L85 35 L95 24 L108 25 L115 35 L129 31Z"/>
<path fill="#2e1610" fill-rule="evenodd" stroke="#2e1610" stroke-width=".7" stroke-linejoin="round" d="M140 114 L145 121 L147 117Z M133 100 L138 111 L141 106 L147 104 L142 98Z M177 89 L171 94 L169 105 L175 114 L188 115 L193 108 L191 96 L185 90Z M181 93 L188 96 L186 103 L178 100Z M112 77 L104 76 L93 84 L90 96 L95 102 L105 103 L114 96 L117 85Z M107 80 L114 84 L109 91 L103 86Z"/>
<path fill="#663b2b" fill-rule="evenodd" stroke="#663b2b" stroke-width=".7" stroke-linejoin="round" d="M232 183 L240 218 L240 240 L230 267 L243 267 L260 262 L275 252 L279 226 L271 205 L260 193 L248 186Z"/>
<path fill="#c49277" fill-rule="evenodd" stroke="#c49277" stroke-width=".7" stroke-linejoin="round" d="M130 120 L129 124 L134 127 L139 127 L143 123 L139 117Z"/>
</svg>
```

そしてLLMの一発描き。

```svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
  <g>
    <ellipse cx="102" cy="118" rx="56" ry="48" fill="#9a6a43"/>
    <ellipse cx="100" cy="122" rx="36" ry="30" fill="#e8d3b0"/>
    <ellipse cx="154" cy="128" rx="24" ry="14" fill="#7b5236" transform="rotate(20 154 128)"/>
    <ellipse cx="150" cy="128" rx="18" ry="10" fill="#b98a5c" transform="rotate(20 150 128)"/>

    <circle cx="100" cy="78" r="38" fill="#b98a5c"/>

    <circle cx="73" cy="51" r="13" fill="#6b4730"/>
    <circle cx="127" cy="51" r="13" fill="#6b4730"/>
    <circle cx="73" cy="53" r="7" fill="#efdcbc"/>
    <circle cx="127" cy="53" r="7" fill="#efdcbc"/>

    <ellipse cx="83" cy="79" rx="15" ry="18" fill="#4a3429"/>
    <ellipse cx="117" cy="79" rx="15" ry="18" fill="#4a3429"/>
    <ellipse cx="100" cy="92" rx="22" ry="16" fill="#f3e6cf"/>
    <ellipse cx="100" cy="67" rx="20" ry="10" fill="#e7d0ad"/>

    <circle cx="85" cy="77" r="5" fill="#111"/>
    <circle cx="115" cy="77" r="5" fill="#111"/>
    <circle cx="87" cy="75" r="1.8" fill="#fff"/>
    <circle cx="117" cy="75" r="1.8" fill="#fff"/>

    <ellipse cx="100" cy="87" rx="6" ry="4.5" fill="#2d211c"/>
    <path d="M94 93 Q100 98 106 93" fill="none" stroke="#2d211c" stroke-width="2.2" stroke-linecap="round"/>

    <rect x="60" y="146" width="16" height="30" rx="8" fill="#4a3429"/>
    <rect x="82" y="146" width="16" height="30" rx="8" fill="#4a3429"/>
    <rect x="108" y="146" width="16" height="30" rx="8" fill="#4a3429"/>
    <rect x="130" y="146" width="16" height="30" rx="8" fill="#4a3429"/>

    <path d="M63 111 C74 98, 86 100, 96 111" fill="none" stroke="#5f3f2c" stroke-width="8" stroke-linecap="round"/>
  </g>
</svg>
```

---

### 実験環境と成果物

今回使用した主なものは、画像生成モデル、Python、OpenCV、CairoSVG。VTracerは未実行。

実験中に得られたSVGの代表例は以下の通り。

- 自動トレース版：約4.9KB
- 全体を曲線化した版：約12〜16KB
- 顔を再構成した版：約6.2KB

これらの数字は今回の画像と変換設定によるもので、一般的なSVG変換の性能を示すものではない。

実験結果と未実行部分を分けて整理するタヌキ作成の動機を短く整理する変換処理の実装手順を具体化する


## 参考

- https://x.com/podhmo/status/2114392268874580409?s=20
