Deno で画像ファイルの RGBA を読み書きしてモザイクをかける

Deno で画像ファイルを読み込み、RGBA の情報を書き換えて保存する方法になります。

以前、ImageMagick でモザイクをかけようと思ったのですが、自分の求める条件を完全には満たさなかったため、Deno で画像ファイルにモザイクをかけるプログラムを自作してみました。

参考「[ImageMagick でモザイクをもっと綺麗にかける - Qiita](https://qiita.com/kerupani129/items/c8c9f81e2909696ea955)」

# 1. はじめに

## 1.1. ここでのモザイクの条件

- 色を平均した正方形が並ぶようなモザイク (ピクセル化 / Pixelize)
- 指定した大きさごとに正方形にして、画像サイズが割り切れない場合も上手くする

## 1.2. Deno で画像ファイルを読み書きする手段

Deno で画像ファイル関係のライブラリは主に以下の 2 つがありますが、いずれも未完成版のようです。

- [deno_imagemagick | Deno](https://deno.land/x/deno_imagemagick#deno-imagemagick)
- [dpng | Deno](https://deno.land/x/dpng)

そのため今回は Deno.run() を用いて、ImageMagick のコマンドを実行して画像の読み書きをすることにしました。

ここでは ImageMagick は画像の読み書きのために使用し、画像データの加工はしません。

※ ImageMagick を使用したり、コマンドを直接実行することから、外部に公開してるサーバー上で動かすのはお勧めできません。
※サーバー上で使用したい場合にはセキリティ対策をする必要があります。

# 2. ソースコード

Ubuntu で動作確認しています。

```js:pixelize.js
// 
const decoder = new TextDecoder();

// 
const readImageInfo = async path => {

	const p = Deno.run({
		cmd: ['convert', path, '-format', '{ "width": %[fx:w], "height": %[fx:h] }\\n', 'info:-'],
		stdout: 'piped'
	});

	const infoUint8Array = await p.output();

	const { success } = await p.status();
	if ( ! success ) return;

	p.close();

	const infoText = decoder.decode(infoUint8Array);
	const info = JSON.parse(infoText);

	return info;

}

const readImageRgba = async path => {

	const p = Deno.run({
		cmd: ['convert', path, 'rgba:-'],
		stdout: 'piped'
	});

	const rgba = await p.output();

	const { success } = await p.status();
	if ( ! success ) return;

	p.close();

	return rgba;

}

const writeImageRgba = async (path, rgba, width, height) => {

	const p = Deno.run({
		cmd: ['convert', '-size', '' + width + 'x' + height, '-depth', '8', 'rgba:-', path],
		stdin: 'piped'
	});

	await Deno.writeAll(p.stdin, rgba);
	p.stdin.close();

	const { success } = await p.status();
	if ( ! success ) return;

	p.close();

};

// 
const forRange = (begin, end, step = 1) => callback => {
	for (let i = begin; i < end; i += step) {
		callback(i);
	}
};

// 
const [imageInputPath, imageOutputPath, blockSizeStr] = Deno.args;

const blockSize = parseInt(blockSizeStr);

// 
const info = await readImageInfo(imageInputPath);
const rgba = await readImageRgba(imageInputPath);

// 
const pixelSize = 4;

const size = info.width * info.height * pixelSize;
const blockRowSize = info.width * blockSize * pixelSize;
const widthSize = info.width * pixelSize;
const blockWidthSize = blockSize * pixelSize;

const ibm = size;
const ibs = blockRowSize;
forRange(0, ibm, ibs)(ib => {

	const jbm = ib + widthSize;
	const jbs = blockWidthSize;
	forRange(ib, jbm, jbs)(jb => {

		const cm = jb + pixelSize;
		forRange(jb, cm)(c => {

			let pixelCount = 0;
			let pixelColor = 0;

			const jm = Math.min(c + blockWidthSize, ib + widthSize);
			const js = pixelSize;
			const forRangeBlock = callback => forRange(c, jm, js)(j => {

				const im = Math.min(j + blockRowSize, ibm);
				const is = widthSize;
				forRange(j, im, is)(i => {
					callback(i);
				});

			});

			forRangeBlock(i => {
				pixelColor += rgba[i];
				pixelCount++;
			});

			pixelColor /= pixelCount;

			forRangeBlock(i => {
				rgba[i] = pixelColor;
			});

		});

	});

});

await writeImageRgba(imageOutputPath, rgba, info.width, info.height);
```

```sh:使用例
deno run --allow-run pixelize.js image.jpg image_pixelized.jpg 48
```

|image.jpg|image_pixelized.jpg|
|---|---|
|![image.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232067/66fdeafb-0d72-8932-3d73-af2628bef1a3.jpeg)|![image_pixelized.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232067/00db33b7-621a-91d2-639b-b90660ae2137.jpeg)|

# 3. 説明

## 3.1. `Deno.run()` による `convert` コマンドの実行

`Deno.run()` を用いて ImageMagick の `convert` コマンドを実行します。

標準入出力をパイプして情報をやり取りします。

```sh:画像の幅と高さを&nbsp;JSON&nbsp;形式で標準出力
convert <path> -format '{ "width": %[fx:w], "height": %[fx:h] }\n' info:-
```

```sh:画像を生の&nbsp;RGBA&nbsp;データに変換して標準出力
convert <path> rgba:-
```

```sh:生の&nbsp;RGBA&nbsp;データを標準入力、画像ファイルに変換して保存
convert -size <width>x<height> -depth 8 rgba:- <path>
```

参考「[function Deno.run() - deno doc](https://doc.deno.land/https/github.com/denoland/deno/releases/latest/download/lib.deno.d.ts#Deno.run)」
参考「[class Deno.Process - deno doc](https://doc.deno.land/https/github.com/denoland/deno/releases/latest/download/lib.deno.d.ts#Deno.Process)」
参考「[Input Filename - Command-line Processing - ImageMagick](https://imagemagick.org/script/command-line-processing.php#input)」

## 3.2. モザイクの計算

色を平均するだけですが、なるべく速く、あまりメモリを消費せずに計算する方法を探しました (有名なアルゴリズム等は特にない？) 。

まず、色を平均したいブロックの位置を計算して、ブロックごとに平均の色で上書きすることで、計算用のメモリ使用量を小さくできます。

そして、RGBA のデータから目的のピクセルのデータの位置を計算する際に、ピクセルのX, Y 座標をから RGBA データのインデンクスを毎回計算 (index = width * y + x) するよりも、端から順に足し算を用いたほうが計算量を減らせます。

![test01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232067/63557b61-5f4b-6275-d4a4-8d204dead460.png)

結論からいうと、

1. ブロックの Y 座標 (データ位置 ib)
2. ブロックの X 座標 (データ位置 jb)
3. ブロック内の X 座標 (データ位置 j)
4. ブロック内の Y 座標 (データ位置 i)

の順に座標を計算すると、幅や高さの最大値を考慮しながら足し算の繰り返しで上手くピクセルを走査することができます。

他の順番では上手く走査することができません (座標の最大値の計算が上手くてきない) 。

# 4. ImageMagick コマンドのみでモザイクをかける場合との比較

前回の記事で、ImageMagick コマンドのみでモザイクをかけた場合との比較です。

ここではマスクの処理はしません。

参考「[ImageMagick でモザイクをもっと綺麗にかける - Qiita](https://qiita.com/kerupani129/items/c8c9f81e2909696ea955)」(再掲)

## 4.1. ブロック単位で割り切れない領域の色の問題の解決

前回の記事の方法では、ブロック単位で割り切れない領域の色を求めるとき、画面端の色を引き延ばすことで疑似的に求めていましたが、周辺の色が近い色でないと本来の色から離れてしまう問題がありました。

今回の方法ではその問題を解決できています。

画像サイズ 1600px、ブロックサイズ 90px の例：

|元画像<br>(画像の下 1px と右 1px だけが黒)|前回の方法<br>ピクセル化画像|今回の方法<br>ピクセル化画像|
|---|---|---|
|![test2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232067/da135f38-7c2a-35ac-344b-3531577ce3d8.png)|![test2_pixelized.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232067/055ddc85-70fd-a312-9301-0481943f9f87.png)|![test2_pixelized2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232067/6674264c-3552-bbc6-3a47-7c128f5dfadd.png)|

## 4.2. 実行時間

実行時間は変換画像やブロックサイズにより変動するため、しっかり比較することはむずかしいですが、おおよそ実行時間のオーダーは同じようです。

time コマンドによる実行時間の計測結果です (しっかり平均したりしていませんが、参考程度に) 。

画像サイズ 1600px、ブロックサイズ 90px の例：

||前回の方法|今回の方法|
|---|---|---|
|**real**|0m0.945s|0m0.939s|
|**user**|0m0.750s|0m0.672s|
|**sys**|0m0.219s|0m0.281s|

前回の方法では、場合により real 実行時間が半減することがあるようです (ImageMagick か複数の CPU を使用して処理している？) 。