ImageMagick でモザイクをもっと綺麗にかける

ImageMagick でモザイクをかける方法の紹介になります。

少し WEB 上で調べてみると、`-sample` を 2 つ使用する方法が見つかるのですが、この方法だと質が低い画像になります…。

どうしたらきれいにモザイクをかけられるのか、もっと深く調べてみました。

# 1. ここで目的とするモザイクの条件

1. blur のぼかしでなく、Pixelize のような正方形が並ぶモザイクにしたい
2. 入力の画像の色を平均したような出力画像になってほしい
3. ここでは左上基準で一定ピクセルごとに正方形にして、width や height が割り切れない場合は下や右を上手く扱ってほしい
4. 画像の一部だけにモザイクをかけたい

# 2. 結論

## 2.1. ソースコード

動作確認は Ubuntu と ImageMagick 6.8 で行っています。

```sh:partial-pixelize.sh
#!/bin/bash

# 
function pixelize() {

	local -r BLOCK_SIZE="$2"

	local -r INPUT="$1"
	local -r OUTPUT="${INPUT%.*}_pixelized.${INPUT##*.}"

	`convert "$INPUT" -format 'local readonly WIDTH=%w HEIGHT=%h' info:`

	local -r REDUCED_WIDTH=`expr \( $WIDTH + $BLOCK_SIZE - 1 \) / $BLOCK_SIZE`
	local -r REDUCED_HEIGHT=`expr \( $HEIGHT + $BLOCK_SIZE - 1 \) / $BLOCK_SIZE`

	local -r ADJUSTED_WIDTH=`expr $REDUCED_WIDTH \* $BLOCK_SIZE`
	local -r ADJUSTED_HEIGHT=`expr $REDUCED_HEIGHT \* $BLOCK_SIZE`

	convert "$INPUT" \
		-define distort:viewport="${ADJUSTED_WIDTH}x${ADJUSTED_HEIGHT}" -filter point -distort SRT 0 \
		-scale "${REDUCED_WIDTH}x${REDUCED_HEIGHT}" \
		-sample "${ADJUSTED_WIDTH}x${ADJUSTED_HEIGHT}" \
		-extent "${WIDTH}x${HEIGHT}" \
		"$OUTPUT"

	echo "$OUTPUT"

}

#
readonly IMAGE_INPUT="$1"
readonly MASK_INPUT="$2"
readonly BLOCK_SIZE_INPUT="$3"

if [ -z "$IMAGE_INPUT" ] || [ -z "$MASK_INPUT" ] || [ -z "$BLOCK_SIZE_INPUT" ]; then
	echo 'Usage: '`basename $0`' image.png mask.png <Block Size>'
	exit 1
fi

# 
readonly IMAGE_PIXELIZED=`pixelize "$IMAGE_INPUT" "$BLOCK_SIZE_INPUT"`
readonly MASK_PIXELIZED=`pixelize "$MASK_INPUT" "$BLOCK_SIZE_INPUT"`

readonly MASK_BINARIZED="${MASK_INPUT%.*}_binarized.${MASK_INPUT##*.}"
readonly IMAGE_OUTPUT="${IMAGE_INPUT%.*}_partial_pixelized.${IMAGE_INPUT##*.}"

convert "$MASK_PIXELIZED" -transparent black -alpha extract "$MASK_BINARIZED"
convert "$IMAGE_INPUT" "$IMAGE_PIXELIZED" "$MASK_BINARIZED" -composite "$IMAGE_OUTPUT"
```

## 2.2. 実行例

```sh:ターミナル
./partial-pixelize.sh image.jpg mask.png 24
```

|image.jpg|mask.png|image_partial_pixelized.jpg|
|---|---|---|
|![image.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232067/0fa842ca-6772-9c48-7cd6-20096f38bf67.jpeg)|![mask2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232067/20380383-0c59-921a-b797-bbd63a0a9125.png)|![image_partial_pixelized.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232067/42bdf2ca-03ea-47fc-4fc1-f70bf7ed58fa.jpeg)|

# 3. 全体の説明

## 3.1. モザイクの実現方法

- 1. blur のぼかしでなく、Pixelize のような正方形が並ぶモザイクにしたい
    - ImageMagick には Pixelize の機能が (おそらく) ないため、画像の拡大・縮小を利用して実現する
- 2. 入力の画像の色を平均したような出力画像になってほしい
    - 「`-sample` を 2 つ使用する方法」では縮小時に色を平均しないため、代わりに縮小時に `-scale` を用いる
- 3. ここでは左上基準で一定ピクセルごとに正方形にして、width や height が割り切れない場合は下や右を上手く扱ってほしい
    - キャンバスサイズを割り切れる大きさに変更して拡大・縮小を行う
    - `-extent` を使用してしまうと画像の端の背景色を正しく扱えなくなるため、`-distort` を用いた画像端の拡張を行う
- 4. 画像の一部だけにモザイクをかけたい
    - 画像の拡大・縮小では mask を使うことが (おそらく) できないため、全体にモザイクをかけた画像を生成したのち、モザイクをかけていない画像と合成する

## 3.2. 全体の流れ

1. 元の画像とマスクの画像をそれぞれピクセル化する
    1. 画像を縮小する際にサイズが割り切れるようにするために、画像端の色を引き延ばして拡大する ※
    2. 画像を縮小することでピクセルの色を求める
    3. 画像を拡大する
    4. 元の画像サイズになるように、1 で拡大した部分を削除
2. ピクセル化されたマスクで、白と黒の間になっている部分を全て白に統一する (ピクセル状にマスクを拡大する)
3. マスクを用いて元の画像とピクセル化された画像を合成する

※厳密にはモザイクの色が本来期待される平均色と一致しませんが、周囲に似た色が集まっているという条件で、近い色を再現できます。

# 4. 細かい説明

## 4.1. 画像をモザイクのブロックサイズで割り切れるサイズに拡張する

画像がモザイクのブロックサイズで割り切れないと、縮小・拡大した際にブロックサイズが均一ではなくなるため、割り切れるサイズに拡大します。

一般的に、オリジナルの画像の部分を変更せずにキャンバスサイズを拡大する方法として `-extent` を使用する方法がありますが、これは拡大した部分を単一の背景色で塗りつぶすため、その後に画像を拡大・縮小すると画像端の色が劣化します。

`-distort` を使用することで、画像端のピクセルを拡張することが可能です。

※画像縮小時に、理想的な平均色は再現しません。

```sh
		-define distort:viewport="${ADJUSTED_WIDTH}x${ADJUSTED_HEIGHT}" -filter point -distort SRT 0 \
```

参考「[Viewport, Where Distort Looks - Distorting -- IM v6 Examples](https://www.imagemagick.org/Usage/distorts/#distort_viewport)」
参考「[Virtual Pixels - Miscellaneous -- IM v6 Examples](https://www.imagemagick.org/Usage/distorts/#distort_viewport)」

|元画像|画像端を拡張|
|---|---|
|![image2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232067/1ad52683-f0f9-47cc-df64-c6d97ada5557.jpeg)|![image2_pixelized.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232067/93871427-b56e-135f-c827-743febd60ac3.jpeg)
|

## 4.2. 画像の縮小は `-sample` でなく `-scale` を使用する

`-sample` による画像の縮小はドットを単に削除するため、色を平均してくれません。
そのため、縮小に `-sample` を用いて画像を拡大縮小すると、荒い感じのモザイクになります。
`-scale` を使用すると綺麗に縮小されます。

(しっかり理解できていなくて申し訳ないのですが、`-resize` による縮小も実際に試すと綺麗に縮小されなかったため、`-scale` を使用します。)

参考「[ImageMagick リサイズ補間アルゴリズム - Qiita](https://qiita.com/yoya/items/b1590de289b623f18639)」

## 4.3. 画像の拡大は `-sample` を使用する

画像の拡大時は色の補間をせずに単に拡大してほしいため、`-sample` を使用します。

## 4.4. 画像のトリミングは `-extent` を使用して良い

4.1 で拡張した画像の部分を除去します。

`-crop` でもいいかと思いますが、左上基準では `-extent` の方が短く記述できるため `-extent` を使用しました。

(しっかりと仕様を把握していないため、もし `-crop` の方が良い理由がありましたらコメントで教えてください…。)

参考「[-extent geometry - Command-line Options - ImageMagick](https://www.imagemagick.org/script/command-line-options.php?#extent)」

## 4.5. ピクセル化されたマスク画像を 2 値化する

モザイクのマスクは少なくともその領域がピクセル化される必要があるため、白と黒の間の領域を白にします。

```sh
convert "$MASK_PIXELIZED" -transparent black -alpha extract "$MASK_BINARIZED"
```

参考「[Create Mask for color X ? - ImageMagick](https://www.imagemagick.org/discourse-server/viewtopic.php?t=30834)」

|mask_pixelized.png|mask_binarized.png|
|---|---|
|![mask2_pixelized.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232067/3e2093cf-939d-a2d4-5db3-7bb388ed13ea.png)|![mask2_binarized.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232067/e3fd0f44-21b5-aa7e-0d6a-e2637aa4430d.png)|

## 4.6. 2 値化したマスク画像を使用して元画像とピクセル化した画像を合成する

マスク画像ができてしまえば後は `-composite` するだけです。

```sh
convert "$IMAGE_INPUT" "$IMAGE_PIXELIZED" "$MASK_BINARIZED" -composite "$IMAGE_OUTPUT"
```

参考「[Compose -- IM v6 Examples](https://www.imagemagick.org/Usage/compose/)」

# 5. 本質的ではない説明

## 5.1. ここでは FX 式は使用しない

FX 式を使用することで `convert` コマンドの中で画像サイズを計算することができるのですが、FX 式を使用できるオプションと使用できないオプションがあるようだったので、全体を統一して見やすくするために、ここではシェルの変数を使用して画像サイズを計算しています。

参考「[The Anatomy of an FX Expression - The FX Special Effects Image Operator - ImageMagick](https://imagemagick.org/script/fx.php#anatomy)」

## 5.2. 変換したマスク画像をファイルとして保存する

状況によると思いますが、マスク画像を使いまわして複数の画像にモザイクをかけることを想定し、変換したマスクをファイルとして保存しています。

以下のように記述すると変換したマスク画像をファイルとして保存せずに処理するため、マスク画像を使いまわさない想定なら以下の方が良いと思います (ディスク読み書きの時間を短縮できます) 。

```sh:書き換え前
convert "$MASK_PIXELIZED" -transparent black -alpha extract "$MASK_BINARIZED"
convert "$IMAGE_INPUT" "$IMAGE_PIXELIZED" "$MASK_BINARIZED" -composite "$IMAGE_OUTPUT"
```

```sh:書き換え後
convert "$IMAGE_INPUT" "$IMAGE_PIXELIZED" \( "$MASK_PIXELIZED" -transparent black -alpha extract \) -composite "$IMAGE_OUTPUT"
```

## 5.3. ImageMagick はサーバーサイドで使うのはセキリティ的に危険

ImageMagick を既に使ったことがあるひとならご存じかもしれませんが、ImageMagick は脆弱性が大量にあるため、サーバーサイドで使用するのはかなり注意が必要です。

入力画像が安全であると分かったうえで、ローカルで ImageMagick を使用する分には特に注意しなくていいかと思います。

## 5.4. jpg の劣化について

当たり前ですが、jpg 形式で出力すると jpg の圧縮による劣化が生じます。

劣化を避けたければ png 画像を入力してください。

# 6. その他

本記事の方法では、画面端の割り切れない部分のモザイクの色を本来の色と完全には一致させられません。ご了承ください。