SNSに写真を上げる前に、通行人の顔を消したい。ストリートスナップから他人のナンバープレートを隠したい。集合写真を社内ブログに載せるとき、本人以外の顔をぼかしたい。
こういう「写真の中の個人情報を消したい」ニーズは地味に多い。一方で、既存のオンラインツールに画像をアップロードするのは怖い。個人情報を消す目的のツールに、その個人情報を含む画像をアップロードするという構造の矛盾がある。
そこで、ぱんだツールズに「画像のプライバシー保護」用のツールを3本作った。3本ともブラウザ完結で、画像はサーバーに送られない。
| ツール | 用途 | 検出方法 |
|---|---|---|
| モザイク・ぼかし処理 | 画像全体をぼかす | 全体一括 |
| 部分モザイク・ぼかし処理 | 任意の矩形だけ隠す | 手動ドラッグ |
| 顔自動モザイク | 顔を自動検出して隠す | AI(face-api.js) |
この記事は、3兄弟の実装と、特に「モザイクとぼかしの違い」を実装ベースで掘り下げる話。プライバシー保護の用途では、見た目以上にこの違いが重要になる。
モザイクとぼかしと黒塗りの違い(実装ベース)
UI上は「強度スライダーで雰囲気が変わる」だけに見える3つの処理だが、ピクセル操作のレベルではまったく別物。
モザイク(pixelate):縮小 → 拡大
モザイクは、対象領域を一度小さい Canvas に縮小して描画し、それをもう一度元のサイズに拡大する処理。拡大時に imageSmoothingEnabled = false を指定すると、ニアレストネイバー補間でブロック状のピクセルになる。
export function applyMosaicToRegion(
ctx: CanvasRenderingContext2D,
source: CanvasImageSource,
region: ImageRegion,
strength: number,
): void {
if (region.w <= 0 || region.h <= 0) return
const blockSize = Math.max(1, strength * 5)
const temp = document.createElement('canvas')
temp.width = Math.max(1, Math.floor(region.w / blockSize))
temp.height = Math.max(1, Math.floor(region.h / blockSize))
const tempCtx = temp.getContext('2d')! // 実コードでは null チェックあり
tempCtx.imageSmoothingEnabled = false
tempCtx.drawImage(source, region.x, region.y, region.w, region.h, 0, 0, temp.width, temp.height)
ctx.save()
ctx.imageSmoothingEnabled = false
ctx.drawImage(temp, 0, 0, temp.width, temp.height, region.x, region.y, region.w, region.h)
ctx.restore()
}
ポイントは縮小の段階で情報量が物理的に減っていること。100×100 の領域を 10×10 に縮小したら、その時点で1万ピクセルの情報が100ピクセルに圧縮されている。元の99%の情報は捨てられている。これを再度100×100に拡大しても、戻ってこない。
復元困難性は、ブロックサイズが大きいほど高くなる。プライバシー保護にモザイクが向くのはこの非可逆性が理由。
ぼかし(Gaussian Blur):周囲のピクセルと混ぜる
ぼかしは、各ピクセルの値を周囲のピクセルの加重平均で置き換える処理。実装には ctx.filter = 'blur(Npx)' を使う方法と、ImageData を直接いじる方法がある。
ぱんだツールズでは Safari など ctx.filter の挙動が安定しない環境でも動くように、ImageData ベースで自前のガウシアンブラーを実装している。
// 3 回のボックスブラーによる近似ガウシアンブラー(Ivan Kuckir, "Fastest Gaussian Blur")
export function gaussianBlur(
data: Uint8ClampedArray,
width: number,
height: number,
sigma: number,
): void {
if (sigma <= 0 || width <= 0 || height <= 0) return
const buffer = new Uint8ClampedArray(data.length)
for (const size of boxesForGauss(sigma, 3)) {
const radius = (size - 1) / 2
boxBlurH(data, buffer, width, height, radius)
boxBlurV(buffer, data, width, height, radius)
}
}
3回のボックスブラーを重ねることで、計算量を抑えつつ正確なガウシアン分布に近づける近似手法を採用している。
注意点はモザイクとは違い、ぼかしには各ピクセルに元の情報の痕跡が残ること。理論上は逆畳み込み(deconvolution)で部分的に復元できる可能性がある。ナンバープレートをぼかして晒した投稿が、画像処理で復元されて炎上した事例は実際にある。
実際に同じ家族写真を「ぼかし」モードで処理するとこうなる。前述のモザイク版(同じ写真)と比べると、顔の輪郭・髪・服の色味がうっすら残っているのがわかる。見た目は柔らかいが、復元のリスクはモザイクより高い。
雰囲気を壊さず柔らかく隠したいのがぼかし、確実に情報を消したいのがモザイク、と棲み分けるとよい。
黒塗り:fillRect で完全に塗りつぶす
最強の隠蔽は黒塗り。実装はシンプルで、fillRect で対象領域を #000 に塗るだけ。
export function applyBlackoutToRegion(
ctx: CanvasRenderingContext2D,
region: ImageRegion,
): void {
if (region.w <= 0 || region.h <= 0) return
ctx.save()
ctx.fillStyle = '#000'
ctx.fillRect(region.x, region.y, region.w, region.h)
ctx.restore()
}
法的書類のスクリーンショットを共有するときなど、写真の雰囲気を維持する必要がない用途には黒塗りが一番安全。情報が残る余地がゼロ。
比較表
| 処理 | 復元の難易度 | 見た目 | 向いているケース |
|---|---|---|---|
| モザイク | ほぼ不可能(強度10以上) | ブロック状で強い隠蔽感 | 顔・ナンバー・住所など個人情報 |
| ぼかし | 一部復元の余地あり | 柔らかく自然 | 雰囲気を残したい背景・社外秘でない情報 |
| 黒塗り | 完全に不可能 | 文字通り真っ黒 | 法的書類・極秘情報 |
ぱんだツールズの GSC を見ると「モザイク ぼかし 違い」というクエリで月8回くらい検索結果に出ているが、クリック率はゼロ。実装の差まで理解して使い分けている人は意外に少ないということ。プライバシー保護ツールを作るなら、UI に「どっちを選ぶべきか」のガイドを出す価値がある。
3兄弟ツールの使い分け
1. 全体ぼかし(image-blur):1クリックで終わる
画像全体に均一にぼかし/モザイクをかける。一番シンプル。背景写真の人物が小さくたくさん映っている場合や、サムネイル用にざっくり匿名化したいときに使う。
実装は ImageData を一発で gaussianBlur に通すだけ。
2. 部分ぼかし(image-partial-blur):ドラッグで矩形指定
「ここだけ隠したい」を実現するツール。Canvas にドラッグハンドラを仕込んで、矩形の選択結果を ImageRegion[] として保持する。
function applyBlurToRegion(ctx: CanvasRenderingContext2D, region: ImageRegion, strength: number) {
if (region.w <= 0 || region.h <= 0) return
const imageData = ctx.getImageData(region.x, region.y, region.w, region.h)
gaussianBlur(imageData.data, imageData.width, imageData.height, strength * 2)
ctx.putImageData(imageData, region.x, region.y)
}
複数領域を選択できるようにして、最後にまとめて出力に焼き込む。複数箇所のドラッグを保持して一括処理する設計はユーザビリティ的に効く。1箇所ずつ「選択 → 適用 → 次の箇所を選択」を強制すると、5箇所隠したいときに5回操作が必要になり、明らかにストレス。
3. 顔自動モザイク(face-auto-mosaic):AI で顔だけ自動検出
集合写真の20人ぶんの顔を1つずつドラッグするのは現実的ではない。そこで face-api.js の TinyFaceDetector を使って、顔だけを自動検出してエフェクトをかけるツールも作った。
集合写真をアップロードしてボタンを押すだけで、5人ぶんの顔が一括でモザイク化される。1人ずつドラッグする手作業はゼロ。
UI 上の操作は「画像を入れる → エフェクトを選ぶ → 強度と範囲拡張を決める → ボタン1回」で完結する。スマホ・PC とも縦並びの同じレイアウトで、片手で操作できる粒度に揃えてある。
const mod = await import('@vladmandic/face-api')
await mod.nets.tinyFaceDetector.loadFromUri('/models/face-api')
const options = new mod.TinyFaceDetectorOptions({ inputSize: 416, scoreThreshold: 0.5 })
const detections = await mod.detectAllFaces(img, options)
TinyFaceDetector を選んだ理由は モデルサイズ約200KB で済むこと。フル版の SsdMobilenetv1 は 5MB を超えるので、初回読み込みのもたつきが許容できない。「ブラウザに来てから画面が動き始めるまで」のクリティカルパスにモデルロードが乗るので、軽量モデル一択だった。
モデルファイル(tiny_face_detector_model.bin)は自サイトから配信している。CDN や face-api 公式の GitHub Pages から取ってくる手もあったが、
- 外部に画像が送信されないことを謳う以上、外部リソースへのリクエストもゼロにしたい
- CDN 死んだらツールが動かなくなる
の2点で自サイト配信に統一した。/public/models/face-api/ に置いて、loadFromUri('/models/face-api') で参照している。
顔の矩形は「目・鼻・口」だけ囲む
face-api.js が返す矩形は、顔の中心部(目・鼻・口)の周辺だけを囲む。耳や髪、あごまでは含まれない。そのまま矩形にモザイクを適用すると、顔の輪郭が外側にはみ出して個人特定できてしまう。
なので「範囲拡張」のスライダーを UI に出して、検出矩形をデフォルトで20%、最大80%まで外側に広げられるようにした。
function boxToRegion(detection: FaceDetectionLike, maxW: number, maxH: number): ImageRegion {
const { x, y, width, height } = detection.box
const raw: ImageRegion = {
x: Math.max(0, Math.floor(x)),
y: Math.max(0, Math.floor(y)),
w: Math.max(0, Math.ceil(width)),
h: Math.max(0, Math.ceil(height)),
}
return expandRegion(raw, expandRatio / 100, maxW, maxH)
}
前髪まで覆いたいなら30〜50%、完全に頭部全体を隠したいなら50〜80%が目安。
ブラウザ完結にした副次効果
3兄弟とも処理は Canvas API のみで完結する。サーバー処理ゼロ。これが想像以上に効いた。
- Cloudflare Pages の Free プランで動く。 サーバーサイド処理がないので、ホスティング費は実質ゼロ
- 画像のサイズ制限が緩い。 Cloudflare Workers の 25MB 制限などに引っかからない。ローカル PC のメモリだけが上限
- オフラインでも動く。 Service Worker でキャッシュすれば、機内モードでも顔検出が走る
- 「アップロードしないでくれ」のニーズに応えられる。 個人情報が含まれる画像を扱うツールほど、この保証が説得力になる
3つ目は地味に重要で、ツールの説明文に「画像はサーバーに送信されません」と書ける。これは集合写真や子供の写真を扱う層には強く刺さる。
ハマりどころ
1. CSS の filter: blur() は領域指定で挙動が荒れる
最初は ImageData をいじらず、ctx.filter = 'blur(Npx)' を使って領域だけぼかす実装にしていた。これが Safari と Firefox で挙動が安定しない。clip 領域を切ってから filter をかけても、ぼかしの「縁」が領域外にはみ出る。
ツールごとに対応を分けた:
-
全体ぼかし(image-blur)と部分ぼかし(image-partial-blur) は、領域コピー → ImageData 取得 → 自前
gaussianBlur→ putImageData の流れに統一した。CPU 計算は重くなるが、見た目が確実 -
顔自動モザイク(face-auto-mosaic) は
ctx.filter = 'blur(Npx)'を使い続けている。検出される矩形が小さく、はみ出しが視覚的にほぼ気にならないため、コストの低い実装で十分と判断した
「常に自前ガウシアンに統一する」は誤りで、領域サイズと許容できる縁ノイズに応じて使い分けると最適化しやすい。
2. face-api.js は SSR と相性が悪い
face-api.js は tensorflow/tfjs を内部依存している。Next.js のビルド時に SSR バンドルに巻き込まれると、window is not defined でビルドが落ちる。
対策は2つ:
-
動的 import:
await import('@vladmandic/face-api')でクライアント側でしか読み込まない - 型だけ手動定義: ライブラリから型を import すると SSR 側に巻き込まれるので、必要な型だけ手動で書く
// face-api.js の顔検出結果の最小型定義(ライブラリから import すると SSR 側に巻き込まれるため手動定義)
interface FaceDetectionLike {
box: { x: number; y: number; width: number; height: number }
}
3. 高解像度画像で UI が固まる
4000px 以上の画像で TinyFaceDetector を回すと、推論に数秒〜十数秒かかる。Web Worker に逃がすのが本命だが、まずは「処理中…」の表示でユーザーに状態を伝えるところで止めている。今後の改善課題。
まとめ
「モザイク」「ぼかし」「黒塗り」は、UI 上の選択肢としては並列に並ぶ3つのオプションだが、実装と復元困難性のレベルではまったく別物。プライバシー保護を本気でやるなら、ぼかしよりモザイク(強度10以上)か黒塗りを選ぶ判断軸を持っておくとよい。
3兄弟ツールはこちら。全部無料・ブラウザ完結。
部分指定したいときは 部分モザイク・ぼかし処理、複数の顔を一括で処理したいときは 顔自動モザイク を使い分けると効率がいい。
ぱんだツールズ では他にも PDF・画像・CSV・テキスト処理の開発者向けツールを80個以上公開中。全部無料・登録不要・ブラウザ完結。
https://sakutto-panda.com
この記事は Zenn にも同じ内容を投稿しています。


