写真をサーバに送らずに、顔にモザイクをかける仕組み
AI で写真の顔を見つける、と聞くと、写真をどこかのサーバに送って処理してもらうものだと思うかもしれません。
Face Mosaic は違います。顔を見つける AI も、モザイクをかける処理も、すべてあなたのブラウザの中で動きます。写真は一度もサーバに送られません。サーバに置いてあるのは、HTML や JavaScript、AI のモデルといった静的なファイルだけです。
このページでは、それがどうやって動いているのかを説明します。
自分で確かめられます
「送っていません」と書くだけなら誰にでもできます。Face Mosaic では、それをあなた自身で確かめられます。
- パソコンの Chrome で Face Mosaic を開く
- 開発者ツール(Mac は ⌥⌘I、Windows は F12)を開き、「ネットワーク」タブを選ぶ
- 写真を選ぶ
写真を選んだあとに出てくる通信は、AI のモデル(.onnx)と、それを動かすプログラム(.wasm・.mjs)を取りに行く GET だけです。どれも初回だけで、写真を送る POST はどこにもありません。blob: で始まる行は、ブラウザが自分のメモリから写真を読み出した記録で、外への通信ではありません。モデルを読み込んだあとは、機内モードにしても次の写真を処理できます。
サーバに送る方式との違い
よくある方式
- 写真をサーバにアップロードする
- サーバの AI が顔を探す
- 結果を受け取る
写真がネットワークを通り、相手のサーバに届きます。サーバの費用もかかります。
Face Mosaic
- AI のモデルをブラウザに読み込む
- ブラウザの中で顔を探す
- ブラウザの中で隠して保存する
写真は端末から出ません。届くのはモデルのほうです。
写真をサーバに送る代わりに、AI のほうを写真のあるところへ届ける。これが Face Mosaic の考え方です。
ブラウザで AI を動かす
使っている技術
| 役割 | 使っているもの |
|---|---|
| 顔を見つける AI | YuNet(OpenCV Zoo、MIT ライセンス、230KB の ONNX ファイル) |
| AI を動かす | ONNX Runtime Web(Microsoft、MIT ライセンス)の WebAssembly 版 |
| 顔を隠す・保存する | Canvas API |
| 配信 | 静的なファイルだけ。サーバのプログラムはありません |
YuNet は、画像処理のライブラリ OpenCV のプロジェクトが公開している、小さく速い顔検出のモデルです。ONNX という共通の形式で配られているので、ONNX Runtime Web を使えばブラウザでそのまま動かせます。ONNX Runtime Web の本体は WebAssembly で書かれていて、ブラウザの中でも速く計算できます。
モデルを読み込んで動かす部分は、これだけです。
import * as ort from "onnxruntime-web/wasm";
const session = await ort.InferenceSession.create("face_detection_yunet_2023mar.onnx", {
executionProviders: ["wasm"],
});
// input は 1×3×640×640 の画像(後述)
const outputs = await session.run({ [session.inputNames[0]]: input });
速さと精度
顔検出の研究でよく使われる WIDER FACE の検証用データから、集合写真・家族写真・会議など 10 ジャンル 60 枚を選び、写っている顔のうちいくつを隠せたかを比べました。アプリと同じく枠を 20% 広げ、顔の半分以上を覆えたら「隠せた」としています。対象は、写真の長辺の 1.5% 以上の大きさで写っている顔です。
| 方式 | 隠せた顔 | 1 枚あたりの時間 |
|---|---|---|
| YuNet をブラウザで動かす(いまの方式) | 82% | 約 25 ミリ秒 |
| 縮小した写真をサーバに送り、OpenAI の画像対応モデルで調べる | 75% | 約 11 秒 |
| MediaPipe の顔検出(BlazeFace short range)をブラウザで動かす | 18% | 数ミリ秒 |
時間は、手元の Mac の Chrome で測ったものです。初めて使うときだけ、モデルとプログラムの読み込みに 1 秒ほど余分にかかります。同じページで 2 枚目からは、読み込み済みのモデルをそのまま使います。
ブラウザで動く小さなモデルが、写真をサーバに送って大きな AI に調べてもらう方式より、多くの顔を、400 倍以上速く見つけました。
前処理:写真をモデルの入力にする
YuNet は 640×640 ピクセルの画像を受け取ります。写真の長いほうの辺が 640 ピクセルに収まるように縮め、左上に寄せて置き、余ったところは黒で埋めます。縦横の比率は変えないので、細長い写真でも顔がつぶれません。
色は、OpenCV と同じ青・緑・赤の順に並べ、0〜255 の値のまま渡します。
const scale = Math.min(1, 640 / Math.max(width, height));
ctx.fillRect(0, 0, 640, 640); // 余白は黒
ctx.drawImage(image, 0, 0, width * scale, height * scale);
const rgba = ctx.getImageData(0, 0, 640, 640).data;
const plane = 640 * 640;
const data = new Float32Array(3 * plane);
for (let i = 0; i < plane; i++) {
data[i] = rgba[i * 4 + 2]; // B
data[plane + i] = rgba[i * 4 + 1]; // G
data[2 * plane + i] = rgba[i * 4]; // R
}
const input = new ort.Tensor("float32", data, [1, 3, 640, 640]);
後処理:出力を顔の枠にする
YuNet は、画像を 8・16・32 ピクセルの 3 とおりのます目に分け、ます目ごとに「顔らしさ」と「枠の位置」を返します。ます目が細かいほど小さな顔を、粗いほど大きな顔を拾います。これを、OpenCV の FaceDetectorYN と同じ式で枠に直します。
const score = Math.sqrt(clamp01(cls[i]) * clamp01(obj[i]));
if (score < 0.5) continue; // 顔らしさが 50% 未満は捨てる
const cx = (col + bbox[i * 4]) * stride; // 枠の中心
const cy = (row + bbox[i * 4 + 1]) * stride;
const w = Math.exp(bbox[i * 4 + 2]) * stride; // 枠の幅と高さ
const h = Math.exp(bbox[i * 4 + 3]) * stride;
同じ顔に枠がいくつも重なるので、顔らしさの高い順に残し、残した枠との重なり(IoU)が 0.3 を超える枠は捨てます(NMS)。最後に、縮めた分を戻して元の写真の座標にします。
Canvas で顔を隠す
元の写真をそのまま持っておき、設定を変えるたびに Canvas に描き直します。顔を探し直すことはしないので、隠し方を変えるとすぐに切り替わり、何度変えても画質は落ちません。
見つけた枠は、上下左右に少し広げてから隠します(「範囲」の設定。最初は 20%)。枠が少しずれても、顔の端が見えないようにするためです。
| 隠し方 | やっていること |
|---|---|
| モザイク | 顔の部分を数マスまで縮め、なめらかにせずに引き伸ばします |
| ぼかし | 半分ずつ何度も縮めて数ピクセルにし、なめらかに引き伸ばします。一度に縮めると画素が飛んで、顔の細部が残ることがあるためです |
| 絵文字 | 顔を強くぼかしてから絵文字を重ねます。星やハートのすき間から顔が見えないようにするためです |
モザイクは、Canvas の機能だけで作れます。
// 長いほうの辺が cells マスになるまで縮める
tiny.getContext("2d").drawImage(canvas, x, y, w, h, 0, 0, sw, sh);
// なめらかにせずに引き伸ばすと、ブロック状になる
patch.getContext("2d").imageSmoothingEnabled = false;
patch.getContext("2d").drawImage(tiny, 0, 0, sw, sh, 0, 0, w, h);
手動モードの枠や角の丸は、写真の上に重ねたもう 1 枚の透明な Canvas に描いています。保存する画像には入りません。
「画像を保存」は、Canvas を toDataURL("image/png") で PNG にして保存します。画像を新しく描き直しているので、元の写真に入っていた撮影場所(GPS)などの情報は、保存した画像には残りません。
サーバがいらない、ということ
処理がすべてブラウザの中で終わるので、Face Mosaic には、写真を受け取るサーバのプログラムがありません。配っているのは静的なファイルだけです。
| ファイル | 大きさ |
|---|---|
| HTML・JavaScript | 合わせて 200KB 以下 |
| YuNet のモデル | 230KB |
| ONNX Runtime Web の WebAssembly | 約 11MB(通信は圧縮されて 3MB ほど) |
これには、写真が安全なこと以外にもよいことがあります。
- 何人が使っても、サーバの負荷や AI の利用料が増えない:計算するのは、それぞれの利用者の端末です
- API キーがいらない:外部の AI サービスを呼ばないので、キーを守る仕組みも、使いすぎを防ぐ仕組みも要りません
- どこにでも置ける:静的なファイルを置けるところなら、どこからでも配れます。いまは Cloudflare Pages から配っています
できないこと
YuNet は多くの顔を見つけますが、すべてではありません。特に、次のような顔は見落とすことがあります。
- とても小さく写っている顔
- 横を向いている顔
- 手や物で一部が隠れている顔
SNS などに投稿する前に、隠れていない顔がないか必ず確かめてください。見落としがあれば、手動モードで枠を足して隠せます。
ライセンス
- YuNet:MIT ライセンス
- ONNX Runtime Web:MIT ライセンス
全文は ライセンス文 にあります。
写真を選ぶだけで、ブラウザの中で顔を見つけて隠します。
Face Mosaic を使ってみる