1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

画像にモザイク・ぼかしをブラウザだけで入れる — Canvas APIによる部分処理の実装【サーバー送信なし】

1
Posted at

SNS投稿前に顔やナンバープレートを隠したい、スクリーンショットの個人情報をぼかしたい——こうした場面は日常的にある。Photoshopを起動するほどでもないし、オンラインツールに画像をアップロードするのはプライバシーが気になる。

「ブラウザだけで、選択した領域にモザイクやぼかしを入れられるツール」を作った。Canvas APIを使い、画像は一切サーバーに送信しない設計にしている。

この記事では、ぱんだツールズの「部分モザイク・ぼかし処理」ツールの技術的な実装を解説する。Canvas APIでモザイク処理がどう動くのか、ドラッグ領域選択の座標変換はどうするのか、といった話を具体的なコード付きで書いていく。

Canvas APIでモザイクを作る原理:「縮小→拡大」

モザイク処理の本質はピクセレート——画像をブロック状にする処理だ。直感的には「各ブロックの平均色を計算して塗りつぶす」と思うかもしれないが、Canvas APIではもっとシンプルな方法が使える。

一時Canvasに小さく描画し、それを元のサイズに拡大する。 たったこれだけでモザイクが完成する。

function applyMosaicToRegion(
  ctx: CanvasRenderingContext2D,
  img: HTMLImageElement,
  region: ImageRegion,
  strength: number
) {
  const blockSize = strength * 5

  // 一時Canvasを作成(元の領域を blockSize 分の1に縮小)
  const tempCanvas = document.createElement('canvas')
  tempCanvas.width = Math.max(1, Math.floor(region.w / blockSize))
  tempCanvas.height = Math.max(1, Math.floor(region.h / blockSize))
  const tempCtx = tempCanvas.getContext('2d')!

  // ① 元画像の対象領域を一時Canvasに縮小描画
  tempCtx.imageSmoothingEnabled = false
  tempCtx.drawImage(
    img,
    region.x, region.y, region.w, region.h,  // ソース領域
    0, 0, tempCanvas.width, tempCanvas.height  // 縮小先
  )

  // ② 一時Canvasの内容を元のサイズに拡大描画
  ctx.imageSmoothingEnabled = false
  ctx.drawImage(
    tempCanvas,
    0, 0, tempCanvas.width, tempCanvas.height,  // 縮小された画像
    region.x, region.y, region.w, region.h       // 元のサイズに拡大
  )
  ctx.imageSmoothingEnabled = true
}

ポイントは imageSmoothingEnabled = false の設定。これをオフにしないと、ブラウザが補間処理で滑らかにしてしまい、モザイク感が出ない。ニアレストネイバー補間(最近傍補間)にすることで、ピクセルの境界がくっきりしたブロック状の見た目になる。

強度(strength)は 1〜20 の範囲で、blockSize = strength * 5 で変換される。強度1なら5pxブロック、強度20なら100pxブロックだ。個人情報の隠蔽には強度10以上(50pxブロック)が推奨される。

ガウシアンブラーの実装

ぼかし(Gaussian blur)のほうは、Canvas 2D APIの filter プロパティを使う。

function applyBlurToRegion(
  ctx: CanvasRenderingContext2D,
  img: HTMLImageElement,
  region: ImageRegion,
  strength: number
) {
  ctx.save()

  // 対象領域をクリップパスで制限
  ctx.beginPath()
  ctx.rect(region.x, region.y, region.w, region.h)
  ctx.clip()

  // CSSフィルター構文でガウシアンブラーを適用
  ctx.filter = `blur(${strength * 2}px)`
  ctx.drawImage(img, 0, 0)

  ctx.filter = 'none'
  ctx.restore()
}

ctx.clip() で描画範囲を矩形に制限してから、ぼかしフィルター付きで画像全体を描画する。クリップ外には影響しないので、指定領域だけにぼかしがかかる。

ブラーの半径は strength * 2 ピクセル。強度5で10px、強度20で40pxのブラー半径になる。CSSの filter プロパティを使うことで、ピクセル単位の畳み込み演算を自前で実装する必要がない。

ドラッグによる領域選択と座標変換

部分モザイクツールの肝は「画像のどこにモザイクをかけるか」をユーザーがドラッグで指定する部分だ。ここで厄介なのが、CSSで表示されるサイズとCanvasの内部解像度が異なるという問題。

CSSピクセル→Canvasピクセル変換

Canvasは width="3000" height="2000" のような内部解像度を持つが、CSS上は width: 100% で表示している。ユーザーがクリックした座標(CSSピクセル)をCanvasの座標に変換する必要がある。

function getCanvasPoint(
  e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>
): { x: number; y: number } {
  const canvas = canvasRef.current!
  const rect = canvas.getBoundingClientRect()

  // CSS表示サイズとCanvas内部サイズの比率
  const scaleX = canvas.width / rect.width
  const scaleY = canvas.height / rect.height

  // マウスまたはタッチの座標を取得
  let clientX: number, clientY: number
  if ('touches' in e) {
    clientX = e.touches[0]?.clientX ?? e.changedTouches[0]?.clientX ?? 0
    clientY = e.touches[0]?.clientY ?? e.changedTouches[0]?.clientY ?? 0
  } else {
    clientX = e.clientX
    clientY = e.clientY
  }

  return {
    x: (clientX - rect.left) * scaleX,
    y: (clientY - rect.top) * scaleY,
  }
}

getBoundingClientRect() でCanvasのCSS上の位置とサイズを取得し、canvas.width / rect.width でスケール係数を計算する。Retinaディスプレイでもこの方式なら正しく動く。CSS表示サイズとCanvas内部解像度の比率で座標を変換しているため、表示サイズが変わっても座標が正しく対応する。

タッチイベントとマウスイベントの両方に対応するため、'touches' in e で分岐している。タッチ操作では e.preventDefault() を呼んでスクロールの誤動作を防いでいる。

複数領域の管理

選択領域は ImageRegion[] の配列で管理する。

interface ImageRegion {
  x: number
  y: number
  w: number
  h: number
}

ドラッグ完了時に3つのユーティリティ関数を通して領域を確定する。

function commitDrag(drag: DragState) {
  // ① 開始・終了座標から正規化(右上→左下にドラッグしても正しく処理)
  const normalized = normalizeRegion(drag.startX, drag.startY, drag.endX, drag.endY)

  // ② Canvas境界にクランプ(はみ出し防止)
  const clamped = clampRegion(normalized, canvas.width, canvas.height)

  // ③ 最小サイズ(5px)チェック
  if (isValidRegion(clamped)) {
    setRegions((prev) => [...prev, clamped])
  }
}

normalizeRegionMath.min / Math.abs で開始・終了座標を正規化する。ユーザーがどの方向にドラッグしても x, y が左上、w, h が正の値になる。clampRegion はCanvas境界を超えた分を切り詰め、isValidRegion は幅・高さが5px未満の誤クリックを弾く。

これらは src/lib/image/regionUtils.ts に純粋関数として切り出されており、UIコンポーネントから独立してテスト可能な設計になっている。

リアルタイムプレビューの仕組み

ドラッグ中はリアルタイムで選択領域を可視化する必要がある。redrawCanvas 関数が、3つのレイヤーを重ねて描画する。

const redrawCanvas = useCallback((currentDrag?: DragState | null) => {
  const ctx = canvas.getContext('2d')!

  // レイヤー1: 元画像
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  ctx.drawImage(img, 0, 0)

  // レイヤー2: 確定済み選択領域(青い半透明の矩形)
  ctx.setLineDash([6, 4])
  for (const region of regions) {
    ctx.fillStyle = 'rgba(59, 130, 246, 0.25)'
    ctx.strokeStyle = 'rgb(37, 99, 235)'
    ctx.fillRect(region.x, region.y, region.w, region.h)
    ctx.strokeRect(region.x, region.y, region.w, region.h)
  }

  // レイヤー3: ドラッグ中の一時領域(赤い半透明の矩形)
  if (currentDrag) {
    const r = normalizeRegion(currentDrag.startX, currentDrag.startY, currentDrag.endX, currentDrag.endY)
    ctx.fillStyle = 'rgba(220, 38, 38, 0.2)'
    ctx.strokeStyle = 'rgb(220, 38, 38)'
    ctx.fillRect(r.x, r.y, r.w, r.h)
    ctx.strokeRect(r.x, r.y, r.w, r.h)
  }
}, [regions])

確定済み領域は青、ドラッグ中は赤で表示する。破線ボーダー(setLineDash([6, 4]))で「選択中」であることを視覚的に伝えている。

onMouseMoveonTouchMove のたびにこの関数が呼ばれるが、Canvas 2D APIの描画は十分高速なので、通常の画像サイズであればラグは感じない。

設計上の工夫

ブラウザ完結のプライバシー保護

このツール最大の特徴は、画像がサーバーに送信されないこと。処理フローは以下のとおり。

  1. URL.createObjectURL(file) でローカルファイルのURLを生成
  2. HTMLImageElement に読み込み
  3. Canvas APIで描画・加工
  4. canvas.toBlob() でPNG形式のBlobを生成
  5. URL.createObjectURL(blob) でダウンロードURLを生成

すべてブラウザのメモリ内で完結する。個人情報を含むスクリーンショットや、顔写真を扱う場面でも、外部にデータが出ることはない。ネットワーク通信はゼロだ。

外部ライブラリ不要

モザイク・ぼかし処理に外部ライブラリは使っていない。Canvas 2D APIの標準機能だけで実装している。package.json の依存に画像処理専用ライブラリ(OpenCV.js等)は含まれておらず、バンドルサイズへの影響はほぼゼロ。

メモリ管理

処理中に生成される ObjectURL は、不要になったタイミングで URL.revokeObjectURL() を呼んで解放している。コンポーネントのアンマウント時にもクリーンアップが走る。画像処理ツールはメモリリークしやすいポイントなので、ここは意識的にケアしている。

まとめ

  • Canvas APIのモザイク処理は「一時Canvasに縮小→imageSmoothingEnabled = falseで拡大」というシンプルな原理で実現できる
  • ガウシアンブラーは ctx.filter + ctx.clip() で領域限定のぼかしをかけられる
  • ドラッグの座標変換は getBoundingClientRect() でCSS表示サイズとCanvas内部サイズの比率を計算するのがポイント
  • 領域の正規化・クランプ・バリデーションを純粋関数に切り出すことで、テスタビリティと可読性を両立できる
  • 外部ライブラリなし・サーバー通信なしで、プライバシーに配慮した画像処理ツールが作れる

ぱんだツールズ では他にも PDF・画像・CSV・テキスト処理などの開発者向けツールを公開中。すべて無料・登録不要・ブラウザ完結で使える。
https://sakutto-panda.com


この記事は Zenn にも同じ内容を投稿しています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?