どうも、コンです。
仕事の資料に画像を載せるとき、写真全体ではなく 「文字にモザイク処理をしたい」 という場面がときどきあります。たとえば名簿の電話番号だけ伏せたい、社内ツールのスクショで名前の列だけ隠したい、みたいなケースです。
全体にぼかしをかけてしまうと、何の画像かわからなくなって読者に内容が伝わらなくなる。かといって OCR で文字認識して〜という大掛かりなことまではしたくない。
そんな中間のニーズにちょうどよかったのが、Pillow で矩形を指定してその範囲だけモザイクをかける方法でした。同じことで困っている人の参考になればと思って書きます。
結論(先出し)
やることは 3ステップ だけです。
- 消したい箇所の
(x1, y1, x2, y2)を 目分量で決める - その範囲を
crop()で切り取って、いったん小さくしてから元のサイズに戻す(モザイク化) GaussianBlurでエッジをぼかして、元画像にpaste()で戻す
OCR を使わず、矩形の座標を手動で決めてしまうのがポイントです。レイアウトが固定されたスクショや写真であれば、これで十分に「箇所を限定した伏せ字」ができます。
今回の素材
題材として、Pixabay にあったドイツの駅(Deutsche Bahn)の発車案内板の写真を使います。
「Ziel(行先)」「Gleis(プラットホーム)」「ステータス」の3列構造で、まさに「ここだけ伏せたい」がやりたくなるレイアウトになっています。
今回は左端の 「Ziel(行先)」の文字だけ をモザイクにかけて、プラットホームやステータスはそのまま読める状態を目指します。

ちなみに公開されている駅の表示なので本来マスクは要らないのですが、デモ用に「もしこれが社内画面のスクショだったら」という気持ちで処理してみます。
コード本体
Pillow が入っていない場合はインストールします。
pip install Pillow本体のコードはこれだけです。
from PIL import Image, ImageFilter
SRC = "deutsche-bahn.jpg"
OUT = "deutsche-bahn-masked.jpg"
# モザイクをかけたい矩形 (x1, y1, x2, y2)
ZIEL_BOX = (30, 440, 790, 1540)
PIX = 25 # モザイクの粗さ(大きいほど荒くなる)
BLUR = 8 # ガウシアンぼかしの半径
def obscure(im, box, pix, blur):
region = im.crop(box)
w, h = region.size
# いったん小さくして、最近傍補間で元サイズに戻す(モザイク化)
small = region.resize((w // pix, h // pix), Image.BILINEAR)
region = small.resize((w, h), Image.NEAREST)
# 文字の輪郭が薄く残らないようガウシアンぼかしを重ねる
region = region.filter(ImageFilter.GaussianBlur(radius=blur))
im.paste(region, box)
return im
img = Image.open(SRC).convert("RGB")
img = obscure(img, ZIEL_BOX, PIX, BLUR)
img.save(OUT, quality=85)処理後の結果がこちらです。

「Ziel / Destination」のヘッダ文字、プラットホーム番号、ステータス、画面下部の案内文はそのまま読める状態で、行先の文字だけが消えています。
これだけで「どんな画面か」を伝えつつ「中身は伏せる」ことが両立できます。
中で何が起きているか:BILINEAR と GaussianBlur
コード自体は短いのですが、効きの良し悪しは resize の補間方法と GaussianBlur の組み合わせで決まります。実際に小さなマス目で計算を追ってみます。
Image.BILINEAR(縮小時に使う補間)
例として、4×4 の小さな画像を 2×2 に縮小してみます。元画像の各ピクセルが下のような明るさ(0〜255)を持つとします。
BILINEAR で 1/2 に縮小すると、出力の各ピクセルは 元の 2×2 ブロックの平均になります(縮小倍率がきれいな整数のとき)。たとえば左上の出力ピクセルなら次の計算です。

output(0,0) = (10 + 20 + 50 + 60) / 4 = 35
output(0,1) = (30 + 40 + 70 + 80) / 4 = 55
output(1,0) = (90 + 100 + 130 + 140) / 4 = 115
output(1,1) = (110+ 120 + 150 + 160) / 4 = 135BILINEAR は4個ぜんぶ平均するので、文字の濃い色と背景の薄い色が混ざって 「均された中間色」になり、文字パターンが消えやすいです。
Image.NEAREST(拡大時に使う補間)
続いて、縮小して得た 2×2 を、元のサイズ(4×4)に NEAREST で戻します。NEAREST は 1個の元ピクセルをそのまま正方形ブロックにコピーするだけの単純な方法です。

元の 4×4 と並べてみると、各 2×2 ブロックが その平均値で塗りつぶされた状態になっています。10〜160 まで滑らかに変化していた値が、35 / 55 / 115 / 135 の4色だけに減りました。これがモザイクの正体です。
もし拡大も BILINEAR にしてしまうと、ブロックの境目で値が滑らかに混ざってしまい、いわゆるピクセル感のある「モザイクっぽい見た目」にはなりません。今回のコードが 「縮小は BILINEAR で均す → 拡大は NEAREST でブロック化」という役割分担にしているのは、この見た目を狙ってのことです。
GaussianBlur(モザイク後に薄くかけるぼかし)
NEAREST で拡大した直後は、ピクセルブロックの境目がカクカクした格子として残ります。文字の凹凸がブロックの濃淡として浮き上がることがあるので、最後に薄くぼかしを入れて溶かしています。
GaussianBlur は、各ピクセルを 周辺ピクセルとガウス分布(釣り鐘型)の重み付き平均で置き換える処理です。3×3 の小さなカーネル(重みの行列)として、教科書的にはこんな値がよく使われます。

中央が一番重く(4)、十字方向が次に重く(2)、対角は最も軽い(1)。合計 16 で割って正規化することで、画像全体の明るさが変わらないようにしています。
上の例のような 3×3 領域があったとします(中央だけ暗い)。
中央のピクセルにこのカーネルを適用すると、各セルとカーネルの重みを掛けて足し、最後に 16 で割ります。
new_center = ( 1*100 + 2*100 + 1*100 ← 上の行
+ 2*100 + 4*50 + 2*100 ← 中央の行(中央だけ50)
+ 1*100 + 2*100 + 1*100 ) ← 下の行
/ 16
= ( 100 + 200 + 100
+ 200 + 200 + 200
+ 100 + 200 + 100 )
/ 16
= 1400 / 16
≒ 87.5もともと 50 だった中央の暗いピクセルが 87.5 まで明るくなり、周囲との差が小さくなりました。これが「ぼかし」の正体で、「飛び出した値を周囲に均す」処理です。
マスクする箇所はどう決める?
このやり方の唯一のコツは、マスクしたい矩形の座標を画像から目分量で決めるところです。
私はだいたい、画像をプレビュー(macOS なら Finder のクイックルック、Windows ならフォト)で開いて、ざっくり「ここからここまで」を当てて (x1, y1, x2, y2) をメモ帳に書き出しています。1回コードを回して結果を見て、ずれていたら 10〜20px ずつ動かして再実行、くらいの感覚です。
1回決めてしまえば、同じレイアウトのスクショを何枚並べても同じ座標で回せるので、長い目で見れば OCR を入れるよりも軽くて早いです。
まとめ
- Pillow で「特定の箇所だけマスク」は
crop → resize 縮小 → resize 拡大 → GaussianBlur → pasteの数行で書ける - 縮小は
BILINEARで均し、拡大はNEARESTでピクセル化、最後にGaussianBlurでブロックの境目を溶かす役割分担 - 座標は OCR を使わず、プレビューで目分量で決めて 1〜2回試すのが軽くて早い
- ぼかし半径は 強めに振る(弱いと文字パターンが透ける)
レイアウトが固定された画面(管理ツールのスクショ・プリントアウトの写真など)であれば、毎回 OCR を呼ばなくても、1度だけ目で測って座標を固定する やり方のほうが手軽でした。逆にレイアウトがバラバラな画像を大量に処理するなら OCR ベースに切り替えた方が良さそう、という棲み分けかなと思いました。
画像の出典: Pixabay – Deutsche Bahn Station(撮影:structuro)
ここまで読んでいただきありがとうございました。それでは、また。
