ImageMagick で GIF 処理

ImageMagick で GIF を扱う話です。

# GIF について

古い画像フォーマットですが。GIF アニメーションとして今でもよく使われます。img タグを使ってブラウザでインライン表示出来るのが強いですね。
GIF の特徴としてアニメーションが出来る、色数が最大256色。透明色を使えますが半透明には出来ない。といった点があげられます。

# 減色

256色しか使えないので、色を沢山使った画像を GIF に変換すると画像の劣化が目立ちます。

```shell:
% convert rgbcube.png rgbcube.gif
```

|rgbcube.png | rgbcube.gif|
|---|---|
|<img alt="26040.png" src="https://qiita-image-store.s3.amazonaws.com/0/1798/12d54d08-dec9-6bb7-d2a5-42e1faac6d39.png">| ![26040.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/d997650b-ccd5-4c19-d6e8-989a9ecc6440.gif)|
|![26040-x800-div8-24.png](https://qiita-image-store.s3.amazonaws.com/0/1798/ce51a736-41d2-2625-011f-d8bd42fc9001.png) ← 拡大 | ![26040-x800-div8-24.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/3ee6cd53-7b51-22f5-9484-4888dbca949d.gif) ← 拡大|

この例は極端に悪いケースです。小さい画像だったり特定の色に偏っている場合だと、それほど問題になりません。

# 透明色

透明色を指定できます。ただし、生贄に１色を透明に捧げる必要があります。
以下のコマンドは黄緑(#00d342)を透明色にするコマンドです。

```shell:
% convert Opaopa.png -transparent "#00d342" Opaopa-transparent.gif
```
| Opaopa.png | Opaopa-transparent.gif |
|---|---|
|![Opaopa.png](https://qiita-image-store.s3.amazonaws.com/0/1798/36246254-3e24-930c-e329-bd60372d0b10.png) |![Opaopa-transparent.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/c417e403-d20e-d235-089c-1a5e4dee6b0d.gif)|

この画像では #00d342 色のピクセルを追加すると、そこも透明になります。
つまり、透明色を使うと 255色しか使えなくなります。まぁ256が255に減っても。。という感じです。

# GIF インターレース

ファイルの先頭から

- 8行ごとに1行 ＞ 4行ごとに1行 ＞ 2行ごとに1行 ＞ 残り全部

の順番にピクセルを並べる事で、ネットワークが遅い場合でもはじめに全体像を表示してから、データが取得しながら少しずつ細部を表示できます。
![Opaopa-interlace.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/36cdaee2-33c4-5fb3-3f42-41b17749b8de.gif)

| ピクセルの埋め方と x8拡大 | 実際の表示と x8拡大 |
|---|---|
|![Opaopa-dot1-interlace-1.png](https://qiita-image-store.s3.amazonaws.com/0/1798/241b1de5-8dec-9028-aff9-d4f53e40b78f.png) ![Opaopa-dot8-interlace-1.png](https://qiita-image-store.s3.amazonaws.com/0/1798/f58035a6-a1a7-f1c1-a7f2-ab008bb2a711.png) | ![Opaopa-dot1-interlace-1-cmpl.png](https://qiita-image-store.s3.amazonaws.com/0/1798/6cdec9b0-41c0-d810-0bc2-82cf7ed63b81.png) ![Opaopa-dot8-interlace-1-cmpl.png](https://qiita-image-store.s3.amazonaws.com/0/1798/482ff2cf-d417-8d78-1e9a-c3b4c6b15b8a.png) | 
|![Opaopa-dot1-interlace-2.png](https://qiita-image-store.s3.amazonaws.com/0/1798/757c33ec-693c-1aa9-57a4-52f5db4e1704.png) ![Opaopa-dot8-interlace-2.png](https://qiita-image-store.s3.amazonaws.com/0/1798/264d0121-f36b-46c7-68ee-bc308da0edef.png) |![Opaopa-dot1-interlace-2-cmpl.png](https://qiita-image-store.s3.amazonaws.com/0/1798/3cb531ca-235c-f446-5d0e-46f64bdf0ffe.png) ![Opaopa-dot8-interlace-2-cmpl.png](https://qiita-image-store.s3.amazonaws.com/0/1798/1d9d7636-c366-59e1-0372-918a65f43da2.png) |
|![Opaopa-dot1-interlace-3.png](https://qiita-image-store.s3.amazonaws.com/0/1798/fe4699d8-cca0-56e9-a90f-4c750695975f.png) ![Opaopa-dot8-interlace-3.png](https://qiita-image-store.s3.amazonaws.com/0/1798/82e6b364-bb41-768e-ae63-9a6a1b80acec.png) | ![Opaopa-dot1-interlace-3-cmpl.png](https://qiita-image-store.s3.amazonaws.com/0/1798/abb5735b-ca4d-3fea-3023-a0f60565a577.png) ![Opaopa-dot8-interlace-3-cmpl.png](https://qiita-image-store.s3.amazonaws.com/0/1798/0a2f0243-f2fe-bfaa-ee09-ce57daef1b05.png) |
|![Opaopa-dot1.png](https://qiita-image-store.s3.amazonaws.com/0/1798/7cd22154-3294-17d3-7cc3-7cca59ab0339.png)  ![Opaopa-dot8.png](https://qiita-image-store.s3.amazonaws.com/0/1798/3187fe13-7d2d-d952-af4b-34fce7bfa3a4.png)  |

黒ピクセルと欠けの区別が出来なくて済みません。。(後で作り直すかも。)

# ページオフセット指定

GIF は描画領域としての Screen と、実際に描画する Image のオフセット情報を別に持ちます。

```shell:
% identify Opaopa-posi.gif
Opaopa-posi.gif GIF 120x72 120x72+50+30 8-bit sRGB 16c 999B 0.000u 0:00.000
```

GIF の右にある 120x72 が画像サイズで、120x72+50+30 がスクリーン情報です。

| Opaopa-posi.gif|
|---|
|![Opaopa-posi.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/e2317a42-197e-fc1f-6363-edef4c5eac1b.gif)|

左上に +50+30 の隙間が出来るはずです。(ブラウザによって微妙に表示が異なります。)

# GIF アニメーション

画像を複数指定すると、それらをコマとした１つのアニメーション画像になります。

![Opaopa-anime-dot1-0.png](https://qiita-image-store.s3.amazonaws.com/0/1798/4804eb62-5bc7-ccad-214b-2ab4819445b5.png) ![Opaopa-anime-dot1-1.png](https://qiita-image-store.s3.amazonaws.com/0/1798/bc15a704-c2e2-54ed-be41-a86ffb325949.png) ![Opaopa-anime-dot1-2.png](https://qiita-image-store.s3.amazonaws.com/0/1798/a9530b76-0da2-487f-8516-5e846f7dba2d.png) ![Opaopa-anime-dot1-3.png](https://qiita-image-store.s3.amazonaws.com/0/1798/81bd9768-5358-3d76-c0f9-4ec5154524fa.png) ![Opaopa-anime-dot1-4.png](https://qiita-image-store.s3.amazonaws.com/0/1798/0e25ef5f-98c8-328c-b5a5-fc7c2f650135.png) ![Opaopa-anime-dot1-5.png](https://qiita-image-store.s3.amazonaws.com/0/1798/45401e76-464e-adb2-6c2d-035cb3367dd2.png) ![Opaopa-anime-dot1-6.png](https://qiita-image-store.s3.amazonaws.com/0/1798/a62c4086-a19c-6384-4780-7a778cf46701.png) ![Opaopa-anime-dot1-7.png](https://qiita-image-store.s3.amazonaws.com/0/1798/44655b85-538f-fdee-2172-7e4d4c835f02.png)

```shell:
% convert Opaopa-anime-dot1-[0-7].png Opaopa-anime-dot1.gif
```
![Opaopa-anime-dot1.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/2b5f176b-edfe-15ac-4555-115136150eba.gif)

それの拡大バージョン

![Opaopa-anime-dot8.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/682c413a-f4aa-8689-3b00-e1ed96f76c6f.gif)

### コマ間隔指定

-delay オプションでコマ間の時間を指定できます。アニメーションの速さになります。
0.01(=1/100)秒単位なので、例えば -delay 100 を指定すると 1 frame/sec です。

```shell:
% convert -delay 100  Opaopa-anime-dot8.gif Opaopa-anime-dot8-delay100.gif
% convert -delay  25  Opaopa-anime-dot8.gif Opaopa-anime-dot8-delay25.gif
```

尚、 -delay オプションは入力画像ファイル名の前で指定します。後ろだと無視される事に注意してください。なお、コマ毎に指定も出来ます。

|-delay 100 (毎秒)| -delay 25 (秒間4コマ)|
|---|---|
|![Opaopa-anime-dot8-delay100.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/5cf0804c-7a35-5607-09fd-fec060ad9530.gif) | ![Opaopa-anime-dot8-delay25.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/40cf2f70-82d5-54fb-776e-4d579c2a4a5b.gif) |

1/100 単位で指定できるなら -delay 1 で 100fps だーー！ 等とやりたくなりますが、ブラウザ表示はそこまで頑張ってくれません。せいぜい数十msec までです。

### ループ回数

ループ回数を指定する事できます。
例えば、ループ回数に 1 を指定すると、ループしない GIF アニメが作れます。なお、0 指定で無限ループになります。

```shell:
% convert -delay 50 -loop 1 Opaopa-anime-dot8.gif Opaopa-anime-dot8-loop1.gif
```

![Opaopa-anime-dot8-loop1.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/73d5c297-95a7-8594-c341-3990ddcda891.gif)

おそらく、この記事をここまで読んだ時には画像は止まっていると思うので、画像を別タブとかで開いてリロードして確認してください。^^;

### 色パレットの Global と Local

GIF の色パレットは全コマ共通の Global カラーテーブル、コマ画像ごとの Local カラーテーブルのどっちでも持つ事ができます。

大昔のガラケー端末では Local カラーテーブルを無視したり、正しくない解釈をする事があり、+map オプションで Global カラーテーブルに一本化する事は、よく行われました。

```shell:
% convert localcolormap.gif +map globalcolormap.gif
```

昔話と片付けても良いですが、Global に一本化するとカラーテーブルのデータ量が減るだけでなく、色数も微妙に減らされて結果として圧縮が効きサイズが減る可能性があり、画質に問題がない範囲でカラーテーブル(パレット)のGlobal化を行う事にある程度の理があります。

ImageMagick は内部的に Global カラーテーブルの仕組みはないので、Global か Local かを調べる事はできません。giftext というツールに頼ると良いです。

```shell:
% giftext Opaopa-anime-dot8.gif | grep "Color Map"
  Has Global Color Map.
  No Image Color Map.
  No Image Color Map.
  No Image Color Map.
  No Image Color Map.
  No Image Color Map.
  No Image Color Map.
  No Image Color Map.
  No Image Color Map.
```

上のような表記であれば、Global に一本化されています。

尚、公式サイトでは giftrans で説明しています。

- http://www.imagemagick.org/Usage/anim_opt/#colortables

```shell:
% giftrans -L speed.gif 2>&1 | grep -c "Local Color Table:"
```

## GIF アニメーションの最適化

さて、このエントリのメインイベントです。
GIF アニメーションは静止画を複数もつだけでなく、動画としての簡単な最適化の機能があります。

まずは最適化手法を２種類紹介して、最後に、それを両方適用する方法も紹介します。

### OptimizeFrame フレーム最適化

変化のあるピクセルを全部囲う四角(いわゆる Dirty Rectangle)でクロップした画像を持つ事で、GIF のデータサイズを減らせます。

```shell:
% convert Opaopa-anime-dot1.gif -layers OptimizeFrame  \
          Opaopa-anime-dot1-optframe.gif
% identify -format "size=%wx%h geom:%g\n" Opaopa-anime-dot1-optframe.gif
size=41x18 geom:41x18+0+0
size=15x5 geom:41x18+4+7
size=14x7 geom:41x18+3+6
size=8x7 geom:41x18+6+6
size=8x7 geom:41x18+6+6
size=5x3 geom:41x18+5+8
size=15x3 geom:41x18+4+8
size=16x5 geom:41x18+3+7
```
![Opaopa-anime-dot1-optframe.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/0bf8e12b-220f-dbaf-dd61-709e64a99ed2.gif)
↓ 拡大
![Opaopa-anime-dot8-optframe.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/016b1212-6214-4260-a8f7-8904963a8766.gif)

これだと分からないので、コマを分解します。

```shell:
% convert Opaopa-anime-dot1-opttrans.gif Opaopa-anime-dot1-opttrans-%d.png
```

|元画像 | フレーム最適化 |
|---|---|
![Opaopa-anime-dot8-0.png](https://qiita-image-store.s3.amazonaws.com/0/1798/165f5a74-4d51-1302-22ef-49f233c8d7ba.png) | ![Opaopa-anime-dot8-optframe-0.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/54b9f95b-9eb8-22bd-e5d0-f8ae7dc9f8d8.gif)|
![Opaopa-anime-dot8-1.png](https://qiita-image-store.s3.amazonaws.com/0/1798/837772a9-8b1d-cd3d-7e62-c38169cb6c84.png) | ![Opaopa-anime-dot8-optframe-1.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/ae8e95d9-1158-f306-9faa-81ef6abfe0f7.gif) |
![Opaopa-anime-dot8-2.png](https://qiita-image-store.s3.amazonaws.com/0/1798/cb6f91aa-a5a0-ad43-ff58-58c89e098543.png) | ![Opaopa-anime-dot8-optframe-2.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/285e6a74-099b-6ced-e18b-857d6edbcb14.gif) |
![Opaopa-anime-dot8-3.png](https://qiita-image-store.s3.amazonaws.com/0/1798/6651373e-da70-5384-d4c5-8736a5e710b1.png) | ![Opaopa-anime-dot8-optframe-3.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/a212a6d2-a81c-f3a9-9598-3b4a5839f89b.gif) |
![Opaopa-anime-dot8-4.png](https://qiita-image-store.s3.amazonaws.com/0/1798/178a8fbc-933d-58ac-b05a-6f09ebfa8c7b.png) | ![Opaopa-anime-dot8-optframe-4.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/588a128a-f5dd-1c27-badb-3e7b83f5c3a3.gif) |

＜以下 5-7 は省略＞

### OptimizeTransparency

GIF には dispose メソッドという指定があり、透明ピクセルの場合に前のコマの色をそのまま使う事ができます。

つまり、こんな感じ。

|元画像 | 透明最適化 |
|---|---|
![Opaopa-anime-dot8-0.png](https://qiita-image-store.s3.amazonaws.com/0/1798/165f5a74-4d51-1302-22ef-49f233c8d7ba.png) |![Opaopa-anime-dot8-opttrans-0.png](https://qiita-image-store.s3.amazonaws.com/0/1798/2a71d404-e8d8-8602-a3e6-904345c893b8.png) |
![Opaopa-anime-dot8-1.png](https://qiita-image-store.s3.amazonaws.com/0/1798/837772a9-8b1d-cd3d-7e62-c38169cb6c84.png) | ![Opaopa-anime-dot8-opttrans-1.png](https://qiita-image-store.s3.amazonaws.com/0/1798/a47d97ad-a8f1-ed7d-e518-cdb1e5e21da6.png)|
![Opaopa-anime-dot8-2.png](https://qiita-image-store.s3.amazonaws.com/0/1798/cb6f91aa-a5a0-ad43-ff58-58c89e098543.png) | ![Opaopa-anime-dot8-opttrans-2.png](https://qiita-image-store.s3.amazonaws.com/0/1798/f211f7ee-d0cf-de1c-5b32-0ae637ba8d33.png)|
![Opaopa-anime-dot8-3.png](https://qiita-image-store.s3.amazonaws.com/0/1798/6651373e-da70-5384-d4c5-8736a5e710b1.png) | ![Opaopa-anime-dot8-opttrans-3.png](https://qiita-image-store.s3.amazonaws.com/0/1798/d572900d-7316-0b42-46cf-45404c5c680c.png)|
![Opaopa-anime-dot8-4.png](https://qiita-image-store.s3.amazonaws.com/0/1798/178a8fbc-933d-58ac-b05a-6f09ebfa8c7b.png) | ![Opaopa-anime-dot8-opttrans-4.png](https://qiita-image-store.s3.amazonaws.com/0/1798/ef25ae58-a20f-b2ea-64a2-df72534a2a95.png)|

ただし、コマの大きさ自体は変わっていないので、フレーム最適化ほどはサイズは減らないと思われます。
また、GIF 画像に元々透明ピクセルが含まれている場合は避けた方が良いです。透明ピクセルと「前のコマと同じ色」が区別出来ないので、「非透明 > 透明」の変換をするピクセルが、非透明のままになるでしょう。

### (最強の) Optimize

Optimize Frame と　Optimize Transparency が合わさり最強となった Optimize オプションがこちらです。

```shell:
% convert Opaopa-anime-dot1.gif -layers Optimize  \
          Opaopa-anime-dot1-optimize.gif
```

|元画像 | 透明最適化 |
|---|---|
![Opaopa-anime-dot8-0.png](https://qiita-image-store.s3.amazonaws.com/0/1798/165f5a74-4d51-1302-22ef-49f233c8d7ba.png) |![Opaopa-anime-dot8-optimize-0.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/135678a0-fdec-cabb-1244-c4757e956631.gif)  |
![Opaopa-anime-dot8-1.png](https://qiita-image-store.s3.amazonaws.com/0/1798/837772a9-8b1d-cd3d-7e62-c38169cb6c84.png) | ![Opaopa-anime-dot8-optimize-1.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/c40585cc-2691-86f3-bb86-1c21a91ca874.gif)
|![Opaopa-anime-dot8-2.png](https://qiita-image-store.s3.amazonaws.com/0/1798/cb6f91aa-a5a0-ad43-ff58-58c89e098543.png) |![Opaopa-anime-dot8-optimize-2.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/5af7c91f-2238-83f7-c120-819b6c872d29.gif)  |
![Opaopa-anime-dot8-3.png](https://qiita-image-store.s3.amazonaws.com/0/1798/6651373e-da70-5384-d4c5-8736a5e710b1.png) |![Opaopa-anime-dot8-optimize-3.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/d221040d-809c-24fc-3a72-685268c59733.gif)  |
![Opaopa-anime-dot8-4.png](https://qiita-image-store.s3.amazonaws.com/0/1798/178a8fbc-933d-58ac-b05a-6f09ebfa8c7b.png) |![Opaopa-anime-dot8-optimize-4.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/1b294a82-c6ef-bab3-9d68-545e6267d6fe.gif)  |

見た目わかりにくいですが、ちゃんと画像を最小限の大きさでクロップし、かつ前のコマと同じ色を透明にしています。

```shell:
% ls -l Opaopa-anime-dot1.gif Opaopa-anime-dot1-optimize.gif
-rw-r--r--  1 yoya  staff  16414 12 22  2016 Opaopa-anime-dot1-optimize.gif
-rw-r--r--  1 yoya  staff  17457 12 22  2016 Opaopa-anime-dot1.gif
```

残念ながら、この例だと画像サイズ自体小さいので殆ど減りませんが、もっと大きな画像であれば劇的に減る事が多いです。

```shell:
% ls -l Opaopa-anime-dot8.gif Opaopa-anime-dot8-optimize.gif
-rw-r--r--  1 yoya  staff  26706 12 22  2016 Opaopa-anime-dot8-optimize.gif
-rw-r--r--  1 yoya  staff  45632 12 22  2016 Opaopa-anime-dot8.gif
```

-layers Optimize おすすめです。

ただ、透明ピクセルが含まれる GIF は Optimize Transparency と同じ理由で駄目です。諦めて Optimize Frame を使いましょう。

# GIF アニメのコマ分解

## -coalesce

単純に GIF => PNG 変換をすると、これまでに解説したような最適化されたコマがそのまま PNG に出力されます。
実際に表示される本来のコマを出力したい場合は、-coalesce を使います。

- Coalesce - fill out frames completely
  - https://www.imagemagick.org/Usage/anim_basics/#coalesce

```shell:
% convert Opaopa-anime-dot1-opttrans.gif -coalesce \
          Opaopa-anime-dot1-coalesce-%d.png
```

ちなみに -layers coalesce でも同じです。

## -deconstruct

海外の古いブログだと -deconstruct が紹介される事がありますが、これは最適化された GIF で機能しません。

- Resizing GIF with both -coalesce and -deconstruct produces broken output 
  - https://github.com/ImageMagick/ImageMagick/issues/3545
- Deconstruct 
  - https://www.imagemagick.org/Usage/anim_basics/#deconstruct