PHP imagick convolveImage の引数の変更

PHP imagick の convolveImage は画像の畳み込み処理するメソッドです。
この convolveImage は、imagick 3.3.0 => 3.4.0 を境に受け取る引数の形式が変わりましたが、公式マニュアルが分かりにくいのでメモを作りました。

- 公式マニュアル
  - https://www.php.net/manual/ja/imagick.convolveimage.php

# convolveImage の機能

画像に対して畳み込みの処理ができます。
例えば、以下のような畳み込みカーネルを与えるとブラー効果を得られます。

```math
\frac{1}{16}
\begin{vmatrix}
1 & 2 & 1 \\
2 & 4 & 2 \\
1 & 2 & 1 \\
\end{vmatrix}
```

| 元画像 | convoveImage 適用後 |
|---|---|
![input.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/163df4ef-d139-0022-ec7c-1ef7c0d15e5f.jpeg) | ![output.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/27c675c2-bd55-6251-e6b7-2b21affeb38a.jpeg) |
| ![rg-grid.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/332326e9-4cdf-354c-2e1b-1b0a22a53a7b.png) | ![blur.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/e26e84fa-6dbb-c8f8-0406-726b16e914c5.png) |


この 3x3 で 9個のカーネル係数を引数に渡す方法が imagick や ImageMagick のバージョンによって異なります。

# バージョンによる差異

## imagick 3.3.0 以前

カーネル係数を一次元の配列で渡します。個数は N^2 に限定されます。以下の例だと 3^2 = 3x3 で 9 個の要素を持つ配列です。

```php
$kernel = [1/16, 2/16, 1/16,
           2/16, 4/16, 2/16,
           1/16, 2/16, 1/16];
$image->convolveImage($kernel);
```

convolveImage は渡された配列を正方行列にして処理します。
今回は 9個の配列を渡したので、3x3 行列として処理します。
25個を渡すと 5x5 です。つまり縦長や横長のカーネルは使えません。

## imagick 3.4.0 以降

２次元の配列を ImageKernel オブジェクトで渡します。

```php
$matrix = [ [1/16, 2/16, 1/16],
            [2/16, 4/16, 2/16],
            [1/16, 2/16, 1/16] ];
$kernel = \ImagickKernel::fromMatrix($matrix);
$image->convolveImage($kernel);
```

## ImageMagick 6

ImageMagick 6 では imagick 3.4.0 以降だとしても imagick 3.3.0 以前の引数形式のままになります。

```php
$kernel = [1/16, 2/16, 1/16,
           2/16, 4/16, 2/16,
           1/16, 2/16, 1/16];
$image->convolveImage($kernel);
```

# バージョン分岐

新旧のバージョンに対応する場合、以下のように分岐すると良いでしょう。

```php
$imVersion = \Imagick::getVersion();
if (($imVersion["versionNumber"] < 0x700) || (phpversion("imagick") < "3.4.0")) {  // 古い方
    $kernel = [1/16, 2/16, 1/16,
               2/16, 4/16, 2/16,
               1/16, 2/16, 1/16];
    $image->convolveImage($kernel);
} else {  // 新しい方
    $matrix = [ [1/16, 2/16, 1/16],
                [2/16, 4/16, 2/16],
                [1/16, 2/16, 1/16] ];
    $kernel = \ImagickKernel::fromMatrix($matrix);
    $image->convolveImage($kernel);
} 
```

# バージョンを間違えた場合

以下のようなエラーメッセージが出るはずです。

>
Imagick::convolveImage() expects parameter 1 to be object, array given

>
Imagick::convolveImage() expects parameter 1 to be array, object given

# 何故 ImageMagick 6 だと古いままなのか

ImageMagick 6 の convolve API が正方行列しか対応していないためです。

- ImageMagick 7 用 imagick 実装

```c:imagick-3.6.0/imagick_class.c
        kernel = Z_IMAGICKKERNEL_P(objvar);

        IMAGICK_KERNEL_NOT_NULL_EMPTY(kernel);
        status = MagickConvolveImageChannel(intern->magick_wand, channel, kernel->kernel_info);
```

- ImageMagick 6 用 imagick 実装

```c:imagick-3.6.0/imagick_class.c
        order = (unsigned long) sqrt(num_elements);
        status = MagickConvolveImageChannel(intern->magick_wand, channel, order, kernel);
        efree(kernel);
```

サイズ情報として rank(整数値) しか渡してないので、縦と横を別にできません。

# 備考

## マニュアルの記述

マニュアルが間違えているのは imagick 実装の PHPDoc コメントによるところもあるので、先月(2022/1/10)に修正 PR を送ってあります。なお、今のところ反応はありません。

- fixing to doc comment proto convolveImage #534
  - https://github.com/Imagick/imagick/pull/534

## セパラブルフィルタ

ImageMagick 7, imagick 3.4.0 以降の引数形式だと、縦と横のサイズを任意にできるので、例えば以下のような横だけブラーが最小限の処理で可能です。

```php
$matrix = [ [1/4, 2/4, 1/4] ];  // horizontal blur
$kernel = \ImagickKernel::fromMatrix($matrix);
$image->convolveImage($kernel);
```

```math
\frac{1}{4}
\begin{vmatrix}
1 & 2 & 1 \\
\end{vmatrix}
```

| 元画像 | 横ブラー |
|---|---|
![input.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/163df4ef-d139-0022-ec7c-1ef7c0d15e5f.jpeg) | ![th.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/eba3af95-9dd4-61fd-d614-48e9f24abd49.jpeg)|
| ![rg-grid.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/332326e9-4cdf-354c-2e1b-1b0a22a53a7b.png) | ![blur-hori.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/b997075b-a2fa-91ad-3fa1-013b6e753581.png) |


横でブラーをかけた後、更に縦ブラーもかければ、当初の 3x3 行列と同等の結果が得られます。

```math
\frac{1}{4}
\begin{vmatrix}
1 \\
2 \\
1 \\
\end{vmatrix}
```


```php
$matrix1 = [ [1/4, 2/4, 1/4] ];  // horizontal blur
$matrix2 = [ [1/4],
             [2/4],
             [1/4] ];  // vertical blur
$kernel1 = \ImagickKernel::fromMatrix($matrix1);
$kernel2 = \ImagickKernel::fromMatrix($matrix2);
$image->convolveImage($kernel1);  // horizontal blur
$image->convolveImage($kernel2);  // vertical blur
```

| 元画像 | 横ブラー |
|---|---|
![input.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/163df4ef-d139-0022-ec7c-1ef7c0d15e5f.jpeg) | ![hori.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/eba3af95-9dd4-61fd-d614-48e9f24abd49.jpeg)|
| 縦ブラー | 横-縦ブラー | 
![vert.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/86c496d7-9418-a89b-9487-e71d0b861d1d.jpeg) | ![horivert.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/a06bb4c3-2b34-27da-67a3-d97ca69110d0.jpeg)|

| 元画像 | 横ブラー |
|---|---|
| ![rg-grid.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/332326e9-4cdf-354c-2e1b-1b0a22a53a7b.png) | ![blur-hori.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/b997075b-a2fa-91ad-3fa1-013b6e753581.png) |
| 縦ブラー | 横-縦ブラー |
|![blur-vert.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/388762c2-51c2-9d77-7781-30970c0c9348.png) | ![blur-hori-vert.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/54b53d79-d9ed-49b8-0a48-4185b87d811b.png) |


3x3 だと微妙ですが、それより巨大になればなるほど、縦と横で分けた方が高速化できます。セパラブルフィルタと呼ばれます。

## 1次元配列の N^2 縛り

古い引数の取り方だと、1次元配列で N^2 に個数が限定されると説明しました。
これには少し注意が必要で、imagick 実装を見てみると、

```c:imagick-3.6.0/imagick_class.c
order = (unsigned long) sqrt(num_elements);
status = MagickConvolveImageChannel(intern->magick_wand, channel, order, kernel);
```

なので、N^2 に合わない場合はエラーにならず、後ろの方の要素を切り捨てるようです。
例えば 10個要素の配列を渡した場合は 9 つだけ使われるように思われます。(未検証)

# まとめ

- imagick 3.3.0 以前、又は ImageMagick 6 だと、1次元配列を引数にとる。正方行列として解釈される。
- imagick 3.4.0 以前、かつ ImageMagick 7 だと 2次元配列を包んだ ImageKernel を引数に取る。任意の形の行列を渡せるのでセパラブルフィルタに便利。
- ImageMagick 6 を使っている場合、出来れば ７ にあげましょう。