ImageMagick の画像 Crop

ImageMagick の画像抜き出しについてのうんちくです。

画像の特定領域の抜き出しは、主に -crop と -trim が使われます。条件によって -shave も便利です。

# -crop

画像を抜き出す素直なオプションは -crop です。
-crop (切り取りサイズ)+(切り取り場所の左上)のように使います。

## (width)x(height)+(x)+(y)

```shell
% identify -format "%wx%h" 4x4.png
200x200
% convert 4x4.png -crop 100x100+50+50  out.png
```

| 4x4.png | -crop 100x100+50+50 | out.png |
|---|---|---|
| ![4x4.png](https://qiita-image-store.s3.amazonaws.com/0/1798/47e72e4a-1c4e-ec68-353f-5dced672b2cf.png) |![4x4canvas.png](https://qiita-image-store.s3.amazonaws.com/0/1798/07f5d17d-2b29-5645-0d36-4c26ed8cfbc6.png)|![out.png](https://qiita-image-store.s3.amazonaws.com/0/1798/07be93af-e276-b8e2-e3c4-f66b30c1eb9f.png) |

## -gravity

どの角から crop するかは -gravity 指定を使います。
真ん中(center)と、右下(southeast) を抜き出す例です。

```shell
% convert 4x4.png -gravity center -crop 100x100+0+0  center.png
% convert 4x4.png -gravity southeast -crop 100x100+0+0 southeast.png
```

|4x4.png |4x4crop.png|center.png|southeast.png|
|---|---|---|---|
| ![4x4.png](https://qiita-image-store.s3.amazonaws.com/0/1798/47e72e4a-1c4e-ec68-353f-5dced672b2cf.png)|![4x4-crop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/7ab07085-aa7f-38b0-a1df-734e1ba57ccf.png) |![center.png](https://qiita-image-store.s3.amazonaws.com/0/1798/b5b2d96c-d995-3696-8810-996fd434149f.png)|![southeast.png](https://qiita-image-store.s3.amazonaws.com/0/1798/a0c02b01-8eba-d96e-1ef5-5cd8530341b1.png)|

gravity と +x+y を併せると、gravity の場所を基準にして、そこからどれだけズラすかを制御できます。
例えば、右下隅の25x25だけ間をあけて抜き出すのはこれです。 -25 でなく +25 である事に注意です。

```shell
% convert 4x4.png -gravity southeast -crop 100x100+25+25 southeast.png
```

| 4x4.png | -gravity southeast -crop 100x100+25+25 | out.png |
|---|---|---|
| ![4x4.png](https://qiita-image-store.s3.amazonaws.com/0/1798/47e72e4a-1c4e-ec68-353f-5dced672b2cf.png) |![southeast.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/8b00d6bf-1033-3ebe-c7dc-b95e190ccf74.png)|![out.png](https://qiita-image-store.s3.amazonaws.com/0/1798/6ed01f21-590d-0428-43aa-1d0d3c3fe342.png)|

少し厄介ですが、gravity で east や south に寄せた場合は。ずらすx,y方向に対して +- が逆になります。
端っこをどの位空けるかの指示だと思うと合理的です。

Gravityの詳細はこちらをご参照ください。

- ImageMagick Gravity 指定
  -  https://qiita.com/yoya/items/b6926f01b7cb90217e07

## +0+0 なし

開始位置無しでサイズだけ指定すると、タイル上に分解されます。

```shell
% convert 4x4.png -crop 50%x50% out.png
```
|-crop 50%x50% | out-0.png | out-1.png | - out-2.png |  out-3.png |
|---|---|---|---|---|
![4x4-crop.png](https://qiita-image-store.s3.amazonaws.com/0/1798/7c174543-9f37-ac7d-5f2a-593245e31722.png)| ![out-0.png](https://qiita-image-store.s3.amazonaws.com/0/1798/120e1f47-c4e0-3a2c-0a5e-70196ee99990.png) | ![out-1.png](https://qiita-image-store.s3.amazonaws.com/0/1798/cf2eabd8-ffd2-bb96-9f5e-57988184e226.png) | ![out-2.png](https://qiita-image-store.s3.amazonaws.com/0/1798/36a18f66-772b-0a7d-2252-8f26aed4ae22.png) |![out-3.png](https://qiita-image-store.s3.amazonaws.com/0/1798/430e2929-cb27-c92e-89b3-e80eb528bbae.png) |

out-%d.gif を指定すれば、out-0.gif, out-1.gif, out-2.gif, out-3.gif が生成されます。

実はこのままだと少しまずい問題が残っています。

## crop 使用上の注意

例えば、タイル状に分解されたPNGファイルを元に GIF アニメーションを作るとおそらく意図した結果にならないでしょう。

```shell
% convert -delay 100 out-[0123].png out.gif
```

|out.gif|
|---|
|![out.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/00641d2d-aac2-da61-9b6b-26224e685aa4.gif)|

先ほど生成した PNG ファイルの中身を identify で確認してみましょう。

```shell
% identify out-[0123].png
out-0.png PNG 100x100 200x200+0+0 8-bit sRGB 5c 354B 0.000u 0:00.000
out-1.png PNG 100x100 200x200+100+0 8-bit sRGB 5c 354B 0.000u 0:00.000
out-2.png PNG 100x100 200x200+0+100 8-bit sRGB 5c 354B 0.000u 0:00.000
out-3.png PNG 100x100 200x200+100+100 8-bit sRGB 5c 354B 0.000u 0:00.000
```

PNG や GIF は画像のサイズと別に、キャンバス(もしくはスクリーン)と呼ばれる表示領域としてのサイズ情報を持ちます。
上記 identify の例だと 200x200 のスクリーンの +{x:0or100}+{y:0or100} に 100x100 画像が配置される事を示します。

| out-0.png | out-1.png | out-2.png | out-3.png |
|---|---|---|---|
| ![out-0.png](https://qiita-image-store.s3.amazonaws.com/0/1798/c5c2fa4c-5b58-8098-f5b9-a3abf6c52f27.png) | ![out-1.png](https://qiita-image-store.s3.amazonaws.com/0/1798/2ebf0309-9b4a-b094-5de7-4d5868d95fb9.png) | ![out-2.png](https://qiita-image-store.s3.amazonaws.com/0/1798/63ff4d7b-07b1-80de-205c-b4af41fd4586.png) | ![out-3.png](https://qiita-image-store.s3.amazonaws.com/0/1798/c0227a41-0d8e-f65f-96f1-705d3c795311.png) |

ImageMagick ではレイヤー機能として存在していて、GIF アニメーションのコマを処理する時に便利です。
それ以外だと面倒なだけなので、とりあえずレイヤーを潰すと良いでしょう。

## +repage の勧め

既に crop した画像のキャンバスと画像のサイズを一致させるには、+repage を使って下さい。

```shell
% mogrify +repage out-[0123].png
% identify out-?.png
out-0.png PNG 100x100 100x100+0+0 8-bit sRGB 5c 326B 0.000u 0:00.000
out-1.png PNG 100x100 100x100+0+0 8-bit sRGB 5c 326B 0.000u 0:00.000
out-2.png PNG 100x100 100x100+0+0 8-bit sRGB 5c 326B 0.000u 0:00.000
out-3.png PNG 100x100 100x100+0+0 8-bit sRGB 5c 326B 0.000u 0:00.000
% convert -delay 100 out-?.png out.gif
```

こうするとおそらく意図通りの PNG 画像と GIF アニメーションになるはずです。
なお、JPEG のようにキャンバスの概念を持たない画像フォーマットだと、勝手に +repage 相当の処理がなされます。

|out.gif|
|---|
|![out.gif](https://qiita-image-store.s3.amazonaws.com/0/1798/4dc35a5f-a676-3458-1290-a4722ccea456.gif)|

殆どのケースでは、-crop の後はセットで +repage を実行した方が安全です。

公式にもこう書かれています。

- https://www.imagemagick.org/Usage/crop/#crop_repage

> Always use "+repage" after any 'crop' like operation.Unless you actually need to preserve that info.
> (訳) "crop" などの操作の後には、常に "+repage" を使用してください。実際にその情報を保存する必要がある場合を除きます。

## +0+0!

実は +repage を使わない方法もありまして。+0+0! のように ! を後ろにつけると、-crop では +repage 相当の動作をします。

```shell
% convert 4x4.png -gravity center -crop 100x100+0+0  center.png
% identify center.png
center.png PNG 100x100 200x200+50+50 8-bit sRGB 5c 354B 0.000u 0:00.000
% convert 4x4.png -gravity center -crop 100x100+0+0!  center.png
% identify center.png
center.png PNG 100x100 100x100+0+0 8-bit sRGB 5c 326B 0.000u 0:00.000
```

ただし、例えば、-resize の ! はアスペクト比固定をやめる目印で全然違う意味で、この手の多義的な指定記号を使うのはあまりお勧めできません。文字数の節約を迫られた場合とかにどうぞ。

## -flatten

あと、キャンバス内の位置も大事で残して表示に反映させたい。という場合は -flatten が使えます。

```shell
% convert 4x4.png -gravity center -crop 100x100+0+0  center.png
% convert center.png -background transparent -flatten center2.png
% convert center.png -background blue -flatten center3.png
```

|center.png|center2.png|center3.png|
|---|---|---|
![center.png](https://qiita-image-store.s3.amazonaws.com/0/1798/b5b2d96c-d995-3696-8810-996fd434149f.png) |![center2.png](https://qiita-image-store.s3.amazonaws.com/0/1798/5bffbebb-5b15-1062-c097-27110e8b1994.png)| ![center3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/dc4e014b-152f-b076-d389-a19e65a66d0b.png) |

# -extent

名前から画像を広げる印象の強いオプションですが、画像を狭める、つまり一部を切り取る事もできます。
こちらは crop 位置の page 情報を維持せず、0,0 原点にフィットします。

```shell:
% magick 4x4.png -extent 100x100+50+50 extent.png
% identify extent.png
extent.png PNG 100x100 100x100+0+0 8-bit sRGB 5c 362B 0.000u 0:00.000
```

|extent.png|
|---|
|![extent.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/cff09827-9ec8-eb07-e264-17336d9e8fae.png)|

むしろ、crop より使い勝手が楽かもしれません。

# -shave

-gravity center 指定の -crop と処理的にほぼ同じで、-shave を使うと周囲から何ピクセル削りたい。という指定ができます。-border や -frame の逆の機能です。

```shell
% convert 4x4.png -shave 50x50 out.png
```

|4x4.png|out.png|
|---|---|
| ![4x4.png](https://qiita-image-store.s3.amazonaws.com/0/1798/47e72e4a-1c4e-ec68-353f-5dced672b2cf.png) |![out.png](https://qiita-image-store.s3.amazonaws.com/0/1798/3b1fe437-f71c-d1dc-c5b8-7d97e256a8fc.png)|

こっちはキャンバスも作り直してくれるので、+repage 無しで大丈夫です。

```shell
% identify out.png
out.png PNG 100x100 100x100+0+0 8-bit sRGB 5c 326B 0.000u 0:00.009
```

# -trim

いい感じに抜き出す auto crop operator として -trim があります。
画像の上下左右の４辺から変化のない領域を削ります。文字通りのトリミングです。


```shell
% convert -size 64x64 xc:red \
          -size 32x32 xc:violet -gravity center -composite test1.png
% convert -size 32x64 xc:red xc:yellow +append \
          -size 32x32 xc:violet -gravity center -composite test2.png
% convert -size 32x32 xc:red xc:yellow +append -size 64x32 xc:blue -append \
          -size 32x32 xc:violet -gravity center -composite test3.png
% convert -size 32x32 xc:red xc:yellow +append \
                   \( xc:blue xc:green1 +append \) -append \
                      xc:violet -gravity center -composite test4.png
% mogrify -trim test[1234].png
```

|| test1.png | test2.png | test3.png | test4.png |
|---|:-:|:-:|:-:|:-:|
|trim前|![test1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/06bbf462-4674-7538-120e-7309a2ee1667.png) | ![test2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/e149f9b4-0c94-cd29-930c-d5af99ee10b3.png) | ![test3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/f9f9f624-1832-90c4-1664-9ee212f21c9b.png) | ![test4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/90b72872-33bc-e463-e24a-4a7c36457623.png) |
|trim後| ![test1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/daf978d3-e34e-fa9a-b877-b5ac41068ee6.png) | ![test2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/ccf79b9b-822a-0f3c-28b6-ad13c078c1f7.png) | ![test3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/f9b7ed0e-d20d-f43a-e0a8-0801abc638e6.png) | ![test4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/fc825b63-2e5c-2b31-3f1f-eb4db4a165b2.png) |

```shell
% convert -trim 4x4m.png out.png
```

|4x4m.png|out.png|
|---|---|
|![4x4m.png](https://qiita-image-store.s3.amazonaws.com/0/1798/393a814d-b80d-fc7f-d704-94818e3e9456.png)|![out.png](https://qiita-image-store.s3.amazonaws.com/0/1798/91ab2721-938e-6ca8-cd10-09f235a18286.png)|

これも元のキャンバスが残っていて、問題があります。

```shell
% identify out.png
out.png PNG 100x100 200x200+50+50 8-bit sRGB 5c 354B 0.000u 0:00.000
```

|out.png|
|---|
|![out.png](https://qiita-image-store.s3.amazonaws.com/0/1798/7bed2d7e-935d-5e9a-18e4-8b9b842826b5.png)|

こちらも +repage を使うと混乱しなくて良いかもしれません。

```shell
% identify out.png
out.png PNG 100x100 200x200+50+50 8-bit sRGB 5c 354B 0.000u 0:00.000
% mogrify +repage out.png
% identify out.png
out.png PNG 100x100 100x100+0+0 8-bit sRGB 5c 326B 0.000u 0:00.000
```

このキャンバス上のレイヤー機能は、GIF アニメーションのコマを処理する時に便利ですが、それは別のエントリで解説します。

## trim アルゴリズム

いい感じ、だと説明になっていないので、具体的な処理についてです。

- 一番左上のピクセルの色を基準にして
  - 画像の左の列から色が一致したら狭める
  - 画像の上の行から色が一致したら狭める

| ![test01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/3c74ee2f-2066-2243-6984-e523c722bce5.png) | ![test01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/363b6a00-119d-59f3-817d-8d22e5fbc7ea.png) | ![test01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/64f7bcdd-4e14-bed9-ff70-bf70f9010ad4.png) | ![test01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/72d5eb10-2ab1-45f2-e693-8c1e6e26b2ae.png) | ![test02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/bf0dfd9e-5867-141c-4cb6-cf4fbf1f7086.png)|
|---|---|---|---|---|
![test02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/d48f31ea-eba4-ca6b-37c0-658b00467b96.png) | ![test02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/7d235be5-e0e0-e100-c13e-663ae6d00f36.png) | ![test02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/29afac31-44ff-ad71-51d5-a5b16c34a34b.png) | ![test02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/f15f6b21-2671-16e7-7766-1950108b0dca.png) | ![test01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/ecc94169-4ff1-0a27-e959-442a6a164481.png)


- 一番右上のピクセルを基準にして
  - 画像の右の列から色が一致したら狭める

|![test03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/04b603c7-1f90-da13-f967-7fac666aee7b.png) | ![test03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/3d62ff8b-817d-2f4d-c4e2-38a1ac9c6c5b.png) | ![test03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/938f53f0-b0f9-878b-b7c3-39581a4348d1.png) | ![test03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/a8af5e60-5a64-e39e-6750-7e24c15f3f1f.png) |![test03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/1cf6bf3a-be67-83f0-fc16-291d5f02987f.png) |
|---|---|---|---|---|

- 一番左下のピクセルを基準にして
  - 画像の下の行から色が一致したら狭める

| ![test04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/32e5f079-97c7-aaae-27f5-cc49331c3b63.png) | ![test04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/4fcd9f16-c12f-8522-a5d1-ec74271ff5a8.png) | ![test04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/46acb8fa-9f4a-06bd-7a25-142bef5f3581.png) | ![test04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/54de1030-6bb9-8423-7656-c5be150845c9.png)  |![test04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/c818334f-c6c2-3f47-a5fd-9d53c5d4ce0e.png)
|---|---|---|---|---|

## ImageMagick の実際のコード

-trim は TrimImage 関数が対応します。
どこまでトリミングするかは GetImageBoundingBox 、その情報を元に画像を切り出すのが CropImage です。

-  magick/transform.c: TrimImage

- トリミングの基準とする色を３隅から取り出す。

```c:magick/transform.c
SetMagickPixelPacket(image,p,GetCacheViewVirtualIndexQueue(image_view),
    &target[0]);
  GetMagickPixelPacket(image,&target[1]);
  p=GetCacheViewVirtualPixels(image_view,(ssize_t) image->columns-1,0,1,1,
    exception);
  if (p != (const PixelPacket *) NULL)
    SetMagickPixelPacket(image,p,GetCacheViewVirtualIndexQueue(image_view),
      &target[1]);
  GetMagickPixelPacket(image,&target[2]);
  p=GetCacheViewVirtualPixels(image_view,0,(ssize_t) image->rows-1,1,1,
    exception);
  if (p != (const PixelPacket *) NULL)
    SetMagickPixelPacket(image,p,GetCacheViewVirtualIndexQueue(image_view),
      &target[2]);
  status=MagickTrue;
  GetMagickPixelPacket(image,&zero);
```

- 色が違うところまで境界を狭める。

```c:magick/transform.c
    pixel=zero;
    for (x=0; x < (ssize_t) image->columns; x++)
    {
      SetMagickPixelPacket(image,p,indexes+x,&pixel);
      if ((x < bounding_box.x) &&
          (IsMagickColorSimilar(&pixel,&target[0]) == MagickFalse))
        bounding_box.x=x;
      if ((x > (ssize_t) bounding_box.width) &&
          (IsMagickColorSimilar(&pixel,&target[1]) == MagickFalse))
        bounding_box.width=(size_t) x;
      if ((y < bounding_box.y) &&
          (IsMagickColorSimilar(&pixel,&target[0]) == MagickFalse))
        bounding_box.y=y;
      if ((y > (ssize_t) bounding_box.height) &&
          (IsMagickColorSimilar(&pixel,&target[2]) == MagickFalse))
        bounding_box.height=(size_t) y;
      p++;
    }
```

RGB 値が多少ずれても一致とみなせるよう -fuzz 指定したパラメータで Similar 関数で色を比較します。

- magick/color.c IsMagickColorSimilar
 
```c:magick/color.c
  if ((p->matte != MagickFalse) || (q->matte != MagickFalse))
    {
      /*
        Transparencies are involved - set alpha distance.
      */
      pixel=(p->matte != MagickFalse ? GetPixelOpacity(p) : OpaqueOpacity)-
        (q->matte != MagickFalse ? q->opacity : OpaqueOpacity);
      distance=pixel*pixel;
      if (distance > fuzz)
        return(MagickFalse);
      /*
        Generate a alpha scaling factor to generate a 4D cone on colorspace.
        Note that if one color is transparent, distance has no color component
      */
      if (p->matte != MagickFalse)
        scale=(QuantumScale*GetPixelAlpha(p));
      if (q->matte != MagickFalse)
        scale*=(QuantumScale*GetPixelAlpha(q));
      if (scale <= MagickEpsilon)
        return(MagickTrue);
    }
```

# 参考 URL

- https://imagemagick.org/script/command-line-options.php#crop
- http://www.imagemagick.org/Usage/crop/#crop
- http://www.imagemagick.org/Usage/crop/#trim_color
- ImageMagick の Geometry 仕様
  - https://qiita.com/yoya/items/704fdfbd881f166125d8