spice picks

エンジニアをしているspiceが色々書きます

画像のモザイク処理をブラウザ上で実現する方法 (JavaScript + Canvas API)

生きていると、モザイクをかけたい写真がたまに出現します。

福岡で食べたもつ鍋定食の写真と、それにモザイク処理をかけた写真
危険な飯テロ、安全な飯テロ

なので、個人開発しているサイトに画像のモザイク処理ができるページを作りました。

cora-pic.com

この記事では、上記ページ実装のため、画像のモザイク処理をブラウザで実現するために色々調べたものをまとめています。

ブラウザ上で画像をいじるにはCanvas APIが便利

Canvasとそこに描いた画像のピクセル操作 についてはMDNにまとめられています。

サンプルコードも充実しており、ここに書いてあることを組み合わせればモザイク処理も可能です。

developer.mozilla.org

おおよそ上記MDNに書いてあるのですが、 Canvas APIで画像のピクセルデータを取得/書込することで画像の編集が実現できます。

Canvasに描画した画像データは、Canavs contextのgetImageData関数によってImageData形式で取得ができ、 書込みはCanvas contextのputImageData(myImageData, dx, dy)で、ImageDataとその描画始点座標を指定するだけです。

ImageDataとは

developer.mozilla.org

ImageDataには ImageData.data,ImageData.width,ImageData.heightプロパティが存在します。(詳しく知りたい人はMDN読んで!)

ImageData.dataは、左上から順にピクセルデータのrgba値が並んでいるUint8ClampedArrayという型つき配列です。

例えば、以下のような2*2ピクセルの画像の場合、

ImageData.dataは以下のデータになります。

[66,133,244,1,0,151,167,1,255,171,64,1,84,65,74,1]

このImageData.dataの値を変えて、前述したputImageDatacanvasに画像を描画すれば、変更したピクセルデータに基づいた画像を作成する事ができます。

ただ、前述した通りこのImageData.dataはただの配列ではなく、Uint8ClampedArrayです。

各要素には0から255の整数しか入れる事ができず、あらかじめ配列の長さも決まっているので、この形式のデータを1から作りたい場合はcanvasContext.createImageData(width, height)という具体にデータの作る必要があるので注意が必要です。

CanvasRenderingContext2D.createImageData() - Web APIs | MDN

ImageDataを軽く理解すれば、モザイク処理を実現するロジックを自前で用意すれだけで、任意の画像についてモザイク処理を行う事ができるぜ!

モザイク処理のロジック

前提として、日常的に使う「モザイク」という単語は、英語で言う「Pixalate」で、画像の粒度を単純に荒くすることでなりたっている(「mosaic」って単語だと意味が違ってくるっぽい)。

このモザイク処理( = Pixalate)をするには、

  • モザイク粒度の大きさを定義して
  • その大きさごとに同じ色を描画する

と言う感じ。

粒度ごとに描画する色は対象とする画像に基づいて選択すれば良さそうです。

ざっと調べた感じ、この時の色をどうするか、明確な定義はないっぽくて、 正方形におさまる部分のどこか1pxの色でも、正方形内の色の平均をでも、それらしいモザイク処理になってました。

処理的にはどこか1pxの色を選択するのが楽だったので、今回は前者の方法で色を決めています。

また、実際にImageData.dataの操作で正方形の描画をするときは、

  • dataはただの1次元配列形式なので、横幅と高さの考慮は自前で行う必要がある
  • 色の変更は画像の横幅を考えて端数処理を行わないと、画像右端の色が左端に来てしまう

などの点に注意が必要です。

実際のロジック

そんなこんなで実際に自分が書いたコードはこんな感じ。

const mosaicPixelSize = getMocaicSize(); // ユーザーの入力によってモザイクの大きさを数値で指定
const ctx = canvas.getContext("2d");
const cWidth = canvas.width;
const cHeight = canvas.height;
const imageData = ctx.getImageData(0, 0, cWidth, cHeight); // 元画像のImageData
if (!imageData) {
  return;
}
const dst = ctx.createImageData(canvas.width, canvas.height); // 新しく作るモザイク画像のImageData
for (let y = 0; y < cHeight; y += mosaicPixelSize) {
  for (let x = 0; x < cWidth; x += mosaicPixelSize) {
    // 左端から漏れなく対象に含める
    if (x % cWidth < mosaicPixelSize) {
      x -= x % cWidth;
    }
    const base = (y * cWidth + x) * 4;
    const rIndex = base;
    const gIndex = base + 1;
    const bIndex = base + 2;
    const aIndex = base + 3;

    const r = imageData.data[rIndex];
    const g = imageData.data[gIndex];
    const b = imageData.data[bIndex];
    const a = imageData.data[aIndex];

    [...new Array(mosaicPixelSize)].forEach((v, yi) => {
      [...new Array(mosaicPixelSize)].forEach((v2, xi) => {
        // 画像の反対側のピクセルは処理しない
        if (
          x % cWidth !== 0 &&
          (y * cWidth + x + xi) % cWidth < mosaicPixelSize
        ) {
          return;
        }
        dst.data[base + yi * cWidth * 4 + xi * 4] = r;
        dst.data[base + yi * cWidth * 4 + xi * 4 + 1] = g;
        dst.data[base + yi * cWidth * 4 + xi * 4 + 2] = b;
        dst.data[base + yi * cWidth * 4 + xi * 4 + 3] = a;
      });
    });
  }
}
ctx.putImageData(dst, 0, 0);

このような具合でモザイク処理を施した画像のデータを作る事ができます。

あとはここで作ったImageDataを描画するCanvasを用意して、前述したputImageDataでデータを入れれば画像ができあがります。

画像をダウンロードなどしたい場合は、CanvastoDataURLメソッドを使うなどしてみましょう。

developer.mozilla.org

以上の諸々をいい感じに行っているのが

cora-pic.com

です。

このページでは、ユーザーの入力でモザイクの粒度も選べるようになっているので、よかったら試してみてね。

モザイク処理、意外と実装できるんですな〜。