🤖 AIとつくる

Claude Code に写真の顔隠しを頼むと何往復もかかる — 顔とテキストの検出器を持たせて解決した記録

(更新: )約7,300字
顔の自動スタンプ・絵画の顔検出・宛名ラベルの住所消去の3例を並べたヒーロー画像

ブログに載せる写真の顔隠しやモザイクを Claude Code に頼むと、加工そのものは一瞬なのに「もう少し左上」「もう少し大きく」の位置調整で何往復もかかる — こどもがきゅうりを丸かじりした記事の写真加工で、実際にスタンプ位置2回・動画ぼかし位置2回・テレビ画面のぼかし範囲3回のやり直しが発生しました。

この記事は、その原因を分析して「AI に座標を目測させるのをやめ、検出器に座標を出させる」方針でツールを作り、どこまで通用してどこで破綻するかを実測した記録です。検証とツール実装は Claude Code が行い、私は方針の相談と結果の確認をしています。

なぜ何往復もかかるのか

原因ははっきりしていて、AI は画像を縮小表示で見て座標を目測し、それを原寸に換算して描画するためです。

  • 初回の位置・サイズの精度が低い (縮小画像からの目測なので)
  • 修正指示が「もう少し左上」のような相対言語になり、1往復ごとに 生成 → 送付 → 確認 が必要
  • 多めに覆えば外しにくくなるが、今度は見せたいところまで隠れてしまう

つまり「目測 → 相対修正」のループが構造的に往復を生んでいます。ここを断つには、座標を目測ではなく機械に出させるのが筋です。

部品は全部既製品で揃う

作ったツールは大きく3系統で、いずれも既製の検出モデルの組み合わせです。

系統使った部品できること
YuNet (OpenCV FaceDetectorYN)顔を自動検出してスタンプ / モザイク / ぼかし
テキストPP-OCRv3 検出モデル (OpenCV)文字の行を検出し、番号指定で黒塗り / モザイク / 消去
人物MediaPipe ImageSegmenter人物だけ残して背景をぼかす

モデルはどれも数百 KB 〜 数 MB で、GPU なしのノート PC (メモリ 8GB の WSL) で1枚 0.1〜0.2 秒程度。導入も pip と数ファイルのダウンロードだけでした。

※以下のサンプル写真はすべてパブリックドメインの素材 (NASA の公式ポートレートなど) です。

顔: 自動検出で位置合わせの往復がゼロになった

顔検出は3種類を比較しました。手元の検証写真11枚 (家族写真。横顔・後頭部・テレビ画面に映った顔などを含む) に対して:

検出器今回の写真11枚での結果本来の持ち味
YuNet隠すべき頭部4件をすべて検出。誤検出はスコア閾値の調整でゼロに軽量 (約230KB) かつ向き・距離に強い
MediaPipe BlazeFace (short-range)横顔・後頭部は検出したが、テレビ画面内の顔を見逃し。誤検出2件スマホの自撮り距離向けのリアルタイム検出。設計上「近くの顔」が守備範囲で、遠い顔や画面内の顔は想定外
Haar カスケード誤検出が多く今回は不採用2001年からある古典で、深層学習なしでどこでも動く軽さが魅力。正面顔のリアルタイム検出という原点の用途なら今でも通用する

どの検出器にも設計上の得意分野があって、今回の「ブログ写真のいろんな向き・距離の顔を漏らさず拾いたい」という用途に一番合っていたのが YuNet だった、という結果です。検出した箱を基準にスタンプやモザイクを描くので、位置合わせの往復がなくなります。横顔・後頭部にわずかに見える頬・テレビ画面の中の顔まで拾いました。

アームストロング船長のポートレートに対する元画像・自動スタンプ・モザイク・背景ぼかしの4パターン比較

面白かったのは絵画でも動いたことです。フェルメールの「真珠の耳飾りの少女」の顔をスコア 0.84 で検出しました。

真珠の耳飾りの少女の元画像と、顔検出してぼかした画像の比較

集合写真 (アポロ11号のクルー3人) も全員検出です。

アポロ11号クルー3人の顔がすべて検出されている集合写真

顔検出の「強い・弱い」

回転とサイズを変えながら実測した結果です。

条件結果
回転 15° / 30° / 45° / 90° / 180°すべて検出 (スコア 0.93 → 0.77。逆さまでも拾う)
顔の大きさ検出解像度の中で幅 20px まで検出。12px で取りこぼし
集合写真 (3人)3/3 検出
完全な後ろ姿 (顔の要素ゼロ)原理的に対象外
15度・45度・90度に回転したポートレートでも顔が検出されている様子

弱点ははっきりしています。遠景の群衆のような小さい顔は漏れ、顔がまったく写っていない後頭部は当然ながら検出できません。後者を隠したいときは、番号やセルでの手動指定に切り替えることになります。

副産物: モザイクの粒度バグ

公開できるサンプルを作っている途中で、モザイクのブロックサイズを固定ピクセルにしていたせいで、大きな顔だとモザイクが細かくなりすぎて顔が判別できたままになるバグが見つかりました。モザイクの粒度は領域サイズに比例させる必要があります (領域の長辺を約12分割にして解決)。サンプルを何パターンか試すのは大事です。

テキスト: 行を検出して「番号で指定」

住所やレシートの個人情報のような文字情報は、テキスト検出モデルで行単位の領域を出し、番号付きのプレビューを作って「2〜6番を隠して」と指定する方式にしました。座標のやりとりが番号のやりとりに変わります。

架空の宛名ラベルの各テキスト行に検出番号が振られ、QRコードにも QR のラベル付き番号が振られたプレビュー画像

文字の行だけでなく、QR コードも専用検出器で同じ番号リストに載ります (上の画像の「8:QR」)。

隠し方は黒塗り・モザイクのほかに、周囲の色で塗り戻す inpaint (消去) が使えます。白背景の文書だと「最初から書いていなかったように」消えるのでいちばんきれいでした。下の1枚目は住所・氏名・電話を inpaint で消し、QR をモザイクにしたもの。品名とお問い合わせ番号は意図的に残しています — 隠すところと見せるところを行単位で選べるのがこの方式の利点です。

宛名ラベルの住所・氏名・電話番号の行だけが消去され、QRコードはモザイクになり、品名と問い合わせ番号は残っている画像 同じ宛名ラベルで住所・氏名・電話番号の行とQRコードにモザイクがかかった画像。モザイクがラベルの傾きに沿っている

このサンプルは架空の宛名ラベルで、ラベル自体が 11° 傾いています。検出は傾いた四角形で返ってくるので、マスクも行の傾きに沿ってかかり、隣の行を巻き込みません。

テキスト検出の「強い・弱い」

条件結果
傾き ±20° まで検出維持。行単位のピンポイントマスク可
傾き 30° 以上検出が崩壊 (行の取りこぼし多発)。先に画像の傾きを直す必要あり
行が密集した文書 (レシート等)検出はできるが番号ラベルが重なって読み違えやすい。実際に1回、隣の行を誤って指定した
QR コード・バーコードテキスト検出では拾えない (文字ではないため)。QR・バーコード専用の検出器を併用して解決

QR コードは見落としやすいポイントだと思います。レシートやチケットの QR には文字で書いてある以上の情報が入っていることがあるので、文字だけ消して安心はできません。テキスト検出が QR を拾わないと分かった時点で、OpenCV の QR 検出器 (QRCodeDetector) とバーコード検出器を組み込み、文字の行と同じ番号リストに「QR」のラベル付きで並ぶようにしました。上の宛名ラベルの QR がモザイクになっているのはこの仕組みです。1つ癖があって、QR 検出は画像の解像度に敏感でした (同じレシートでフル解像度だと見つからず、幅2400px に縮小すると見つかる)。ツール側で複数の解像度を試して統合するようにしていますが、それでも漏れる可能性はあるので、QR が写っていそうな写真は最後に目視で確認しています。

背景ぼかし: 人物だけ残す

人物セグメンテーションを使うと「人物はくっきり・背景だけぼかし」ができます。顔スタンプと重ねがけすれば、以前は手作業で何往復もかけていた「背景ぼかし+顔スタンプ」が全自動の2段で終わります。

ここでは調整に2回かかりました。最初の設定はぼかしが弱く、よく見ればぼけていると分かる程度。ぼかしフィルタを強めた2回目でも、背景の月のクレーターがまだ判別できました。プライバシー目的のぼかしは「雰囲気を柔らかくする」のとは要求が違って、背景に何があったか分からないところまでやる必要があります。最終的にはフィルタを強めるのではなく、画像を数十分の一に縮小してから引き伸ばす方式にして、ディテールが原理的に残らないようにしました。

弱点はこれもはっきりしていて、手に持っている物は人物と見なされずぼけます。きゅうりを持った写真だと、きゅうりまでぼけてしまう。ここが気になる場合は、残したい領域を1回だけ手で指定する必要があります。

それでも外れたときの逃げ道

検出は万能ではないので、外れたときのフォールバックを3段用意しました。どれも「相対言語での修正」を避けて、1往復で終わることを狙ったものです。

  1. グリッド指定 — セル番号付きのプレビューを見て「D4-F6 を隠して」と指定
  2. 複数案から選ぶ — サイズ違いの加工案を1枚に並べて「2番で」と返す
  3. クリックで座標指定 — ローカル HTML に画像を載せてクリックすると、原寸換算の座標が出る
A1からJ13のセルラベル付きグリッドを重ねたポートレート画像 スタンプの大きさが違う4案を番号付きで並べたコンタクトシート

まとめ — 完璧さより「弱点の地図」

出来上がったワークフローはこうなりました。

  1. 顔隠し・背景ぼかしは自動検出で一発 (今回の検証では位置修正の往復ゼロ)
  2. 住所などの文字・QR コードは番号プレビューを見て番号で指定 (1往復)
  3. 検出が外れたらグリッド / 複数案 / クリックで指定 (1往復)

動画は今回のツールの範囲外です。きゅうり記事の動画では「顔が映らない区間の切り出し」と手作業のぼかしを組み合わせましたが、素材選択に頼るのは本意ではなく、静止画と同じ品質で動画にもぼかしを入れられるようにするのが次の課題だと思っています。

道具として完璧になったわけでもありません。小さすぎる顔は漏れる、傾き 30° を超えた文字は取れない、持ち物は背景と一緒にぼける。ただ、どこが強くてどこが弱いかの地図を人間と AI が共有できていれば、弱いところだけ人間が補えます。そしてこの地図は「ここで満足」の線引きではなく、現時点での妥協点です。弱点を課題として管理しておけば、画像をもっとたくさん使うようになったときや、さらに効率化したくなったときに、そこから再開できます。今回は無料で使える部品だけで組んだ結果なので、有料のツールや別のモデルを試す余地もまだ残っています。

実装メモ: ハマりどころと工夫

コードの詳細には立ち入りませんが、同じものを作る人がハマりそうなポイントだけ書き留めておきます (実装は Claude Code)。

EXIF 回転。スマホの縦長写真は「横長の画像+回転情報 (EXIF)」として保存されていることがあり、OpenCV の cv2.imread は回転情報を無視します。そのまま検出すると縦長写真がすべて横倒しで処理される。PIL 側で回転を適用してから渡すのが確実でした。

from PIL import Image, ImageOps
im = ImageOps.exif_transpose(Image.open(path))  # EXIFの回転を反映

検出は縮小画像・座標は原寸換算。スマホ写真は1200万画素あり、そのまま検出すると遅い。長辺1600pxに縮小して検出し、出てきた座標を倍率で原寸に戻します。これで1枚 0.1〜0.2 秒。

スコア閾値は分布を見て決める。検証写真11枚で、正解の検出はスコア 0.60〜0.86、唯一の誤検出 (おもちゃのカゴ) は 0.51 でした。間の 0.55 を閾値にすると誤検出だけが消えます。閾値は理屈で決めるより、手元のデータのスコア分布を見て決めるのが手っ取り早い。

モザイクの粒度は領域サイズに比例させる。ブロックを固定 px にすると、大きい顔ではモザイクが細かくなりすぎて顔が判別できたままになります (実際になった)。「領域の長辺を約12分割」のように相対で決めます。

プライバシーぼかしは縮小→拡大で。ぼかしフィルタを強めても「よく見ると分かる」が残りがちでした。画像を数十分の一に縮小してから引き伸ばすと、ディテールが原理的に残りません。

small = cv2.resize(img, (w // 48, h // 48))   # 情報をここで捨てる
blurred = cv2.resize(small, (w, h))            # 引き伸ばしてなめらかに

QR 検出は解像度に敏感。同じ写真でフル解像度だと見つからず、幅 2400px だと見つかることがありました。複数の解像度で試して結果を統合しています。

斜めの文字はポリゴンで塗る。テキスト検出の結果は傾いた四角形で返ってくるので、画像の軸に平行な矩形で塗ってしまうと、隣の行を巻き込みます。四角形をそのままマスクにして、マスク部分だけ加工後の画像と入れ替えると、行の傾きに沿ったピンポイントのマスクになります。

mask = np.zeros(img.shape[:2], np.uint8)
cv2.fillPoly(mask, [poly], 255)       # 傾いた四角形のマスク
img[mask > 0] = processed[mask > 0]   # マスク部分だけ差し替え

ぼかしの境界はなじませる。矩形のままぼかすと縁がくっきり出て加工感が強くなります。楕円のマスクを作り、マスク自体にもぼかしをかけてから合成すると、境界がなだらかに消えていきます。

文字の消去は1行。OpenCV の cv2.inpaint はマスク領域を周囲の色で補完してくれます。白背景の文書なら「最初から書いていなかったように」消えます。

img = cv2.inpaint(img, mask, 7, cv2.INPAINT_TELEA)

付録: Claude Code とのやりとり (抜粋)

このツールと記事は Claude Code との対話で作りました。どういう指示をして、どう返ってきて、それにどう返したのか — 実際のやりとりの流れを、各場面の背景と一緒に抜粋しておきます。AI に仕事を頼むときの参考になれば。

検証の初日。検討候補を書いた issue だけ用意してあって、私はこのあと作業できない日でした。丸ごと任せてみます。

👤 ブログ主のプロンプト
今日はこのあと作業できないので、できるところまで自分で考えて検証しておいてもらえますか? 検証結果は明日確認しますね。
🤖 Claude Code
検出器3種の比較・ツール試作・検証レポートまで自走。翌朝、比較画像つきで結果を確認できました。

翌日、顔隠しの検証結果に問題がないことを確認したあと、ふと気になって聞いたのがこれです。

👤 ブログ主のプロンプト
今回って顔だけで確認しましたかね? 背景をぼかしたいとか、いろんな用途でも使えますかね?
🤖 Claude Code
加工自体は任意の領域に使えると回答した上で、その場で背景ぼかし (人物セグメンテーション) を実装。「手に持った物はぼける」という制約も一緒に報告してきました。

記事に載せられる公開サンプル (パブリックドメイン画像) を作ってもらったところ、その過程で「モザイクが細かすぎて顔が判別できる」バグが見つかったという報告がありました。それを受けての返しです。

👤 ブログ主のプロンプト
公開サンプルでバグを見つけたということは、もう何サンプルか試したほうがよさそうです。顔のサイズや向きが違うものを探してきて試す感じですね。
🤖 Claude Code
回転・サイズ・集合写真の実測を追加 (この記事の「強い・弱い」表のデータ)。指摘どおり、モザイク粒度のバグはサンプルを増やさなければ見つかりませんでした。

検証レポートの初稿では、QR コードについては「テキスト検出の対象外。情報を含む QR は手動で隠す必要あり」という注意書きで終わっていました。

👤 ブログ主のプロンプト
QR が対象外とのことですが、実際のユースケースではどうしたらいいのでしょうかね? ここだけ具体的方策がなくて、注意するだけになってますね。
🤖 Claude Code
QR・バーコード専用の検出器を組み込んで「番号で指定して隠す」流れに統合。注意喚起で終わっていたものが具体策になりました。

記事ドラフトの初稿の比較表は、採用しなかった検出器について「見逃し」「誤検出多数」という結果だけが並んでいて、その検出器自体がダメなものに見える書き方でした。

👤 ブログ主のプロンプト
比較で負けた検出器がダメに見える書き方は避けたい。頑張って作った人がいるので。逆に私が作ったツールでそんなこと言われたら悲しいので。
🤖 Claude Code
比較表に「本来の持ち味」列を追加しました。

同じく初稿には「動画は加工でがんばるより顔が映らない区間を選ぶのが最強」と書かれていました。実際には、過去記事の動画には区間選択とあわせてぼかしも入れています。

👤 ブログ主のプロンプト
「区間を選ぶのが最強」は事実と違いますね。実際はぼかしも入れたし、区間選択は我慢してるだけ。そこに逃げないでほしい。加工で頑張りたいです。
🤖 Claude Code
事実を修正し、動画への高品質ぼかしを次の課題として明記しました。

背景ぼかしは一度「弱い」と指摘して強めてもらったのですが、それでも背景の月のクレーターが判別できるレベルでした。2回目の指摘です。

👤 ブログ主のプロンプト
まだまだ弱いですね。クレーターとかがまだ見えてます。超極端にぼかせませんかね?
🤖 Claude Code
フィルタを強める方式をやめ、縮小→拡大方式に変更しました (2回目の指摘でようやく期待レベルに到達)。

振り返ると、AI が自走で出してくるのは「動くもの+弱点の正直な報告」までで、どこまでやれば十分か・何を大事にするか (批判をしない、事実に忠実に、妥協に逃げない) は、こちらから伝えて直してもらっています。ただ、これを「毎回伝え続けるしかない」と書きかけたところで、それこそ AI の記憶 (メモリや指示ファイル) に書き込めば済むのではと気づいて、実際にこの記事の執筆中に登録してもらいました。伝える → 定着させる、まで含めてが AI との分担なんだと思います。

参考リンク

同じテーマの記事・体験談:

検出モデル・ライブラリ (一次情報):

サンプル画像の出典 (いずれもパブリックドメイン):