PCのキャプチャ画像を公開しようと思ったとき、画像に機密情報や個人情報が写り込んでいるのでモザイクをかけたいという場面がある。ただモザイクをかけるだけなのに、高機能な画像編集ソフトを起動するのが煩わしいと思ったことはないだろうか。そこで、簡単GUIライブラリのTkEasyGUIを使ってモザイクをかけるだけの専用ツールを自作してみよう。

  • モザイク掛け専用ツールを自作してみよう

    モザイク掛け専用ツールを自作してみよう

GUIライブラリのTkEasyGUIをインストールしよう

モザイク専用ツールを作るのに当たって、簡単に使えるGUIライブラリ「TkEasyGUI」をインストールしよう。ターミナル(WindowsならPowerShell、macOSならターミナル.app)を起動して、次のコマンドを実行しよう。

pip install TkEasyGUI

TkEasyGUIで画像を読み込む方法を確認しよう

今回のプログラムでは、画像ファイルを読み込んで、画面に表示するというのが重要になる。そこで、本格的なツールを作る前に、プログラムを実行したらファイルを選択して、画面に表示するというプログラムを作ってみよう。

"""画像ファイルを選択して表示するサンプル"""
from PIL import Image
import TkEasyGUI as eg

# 画像ファイルを選択 --- (*1)
fname = eg.popup_get_file("画像ファイルを選択してください",
            file_types=(("Image Files", "*.png;*.jpg;*.jpeg;*.gif"),))

# 画像をウィンドウに合うようにリサイズ --- (*2)
img = Image.open(fname)
img.thumbnail((800, 600))

# 画像表示ウィンドウを作成 --- (*3)
layout = [
    [eg.Graph("-cv", canvas_size=(800, 600))],
    [eg.HSeparator()],
    [eg.Button("OK")],
]
window = eg.Window("画像表示", layout)
# 画像を描画 --- (*4)
canvas = window["-cv"]
canvas.draw_image(location=(0, 0), data=img)
# イベントループ --- (*5)
while window.is_alive():
    event, values = window.read()
    if event == "OK":
        break
    print(event, values)

プログラムを実行するには、上記のプログラムを「load_image.py」というファイル名で保存して、次のコマンドをターミナルで実行しよう。

python load_image.py

すると、ファイル選択ダイアログが表示されるので、画像ファイルを選ぼう。すると、画像がウィンドウに表示される。

  • ファイルを選ぶとウィンドウに画像が表示される

    ファイルを選ぶとウィンドウに画像が表示される

プログラムを確認しよう。(*1)では、ファイルダイアログを表示する。ここでは、Pillowライブラリで読み込みできる画像形式とした。

(*2)では、画像をウィンドウに合うように、thumbnailメソッドを利用してリサイズする。

(*3)では、キャンバス(eg.Graph)やボタン(eg.Button)などの要素を持つウィンドウを作成する。GUIライブラリのTkEasyGUIを使う場合、二次元のリストに配置したい要素のオブジェクトを指定する。こうすることで、適当に画面レイアウトを作ってくれるので便利だ。

(*4)では、draw_imageメソッドを利用して画像を描画する。なお、window[キー名]のように書くと、ウィンドウに配置した要素を取得できる。

(*5)では、イベントループを記述して、ウィンドウを表示する。

モザイクがけツールを作ってみよう

ここまでの部分で、簡単な画像を表示するプログラムの基本が分かった。そこで、いよいよモザイクがけツールを作ってみよう。

なお、100行前後の短いプログラムだが抜粋して紹介する。プログラム全体は、こちらのGistで確認できる。

また、本原稿のために作ったツールだが、日常業務で頻繁に利用しそうだったので、こちらのGitHubで独立したオープンソースのプロジェクトとしても公開した。

プログラムを実行するには、プログラムを「mosaic_tool.py」というファイル名で保存して、ターミナルで下記のコマンドを実行しよう。

python mosaic_tool.py

プログラムを実行するとファイルを選択するダイアログが表示される。ファイルを選択するとモザイク編集画面が表示される。マウスでモザイクを掛けたい部分をドラッグで選択すると、モザイクが掛かる。「保存」ボタンを押すとモザイクを掛けた画像を保存できる。なお、モザイクのタイルサイズを画面下部で選択できるようになっている。

  • モザイクツールを実行したところ

    モザイクツールを実行したところ

作成したプログラムを確認しよう

さて、作成したプログラムを少しずつ確認してみよう。以下のプログラムが、定数やグローバル変数を定義したものだ。定数やグローバル変数は、プログラムの先頭にまとめておいて、後で修正がしやすいように配慮するのが大切だ。

# 定数の宣言
CANVAS_SIZE = (800, 600)  # キャンバスのサイズ
MOSAIC_SIZE = 5 # モザイクのサイズ
# グローバル変数
is_mouse_down = False
start_xy = (0, 0)
move_list = []
canvas_img = None  # キャンバスに表示する画像
original_img = None  # 元画像
rate = 1.0  # 画像の拡大率

続いて、以下の部分が、プログラムのメイン部分だ。まずは、プログラムを眺めてみよう。

def main():
    """メイン関数"""
    global canvas_img, original_img, rate
    # 最初に画像ファイルを選択
    fname = eg.popup_get_file("画像ファイルを選択してください",
                file_types=(("Image Files", "*.png;*.jpg;*.jpeg;*.gif"),))
    if fname is None or fname == "":
        eg.popup_ok("画像ファイルが選択されませんでした")
        exit()
    # 画像をウィンドウに合うようにリサイズ
    canvas_img = Image.open(fname)
    original_img = canvas_img.copy()
    org_size = canvas_img.size
    canvas_img.thumbnail(CANVAS_SIZE)
    rate = org_size[0] / canvas_img.size[0]
    # ウィンドウを表示
    show_window()

上記のメイン処理では、先ほど作ったプログラムのように、ダイアログで画像ファイルを選択すると、メインウィンドウが起動する仕組みにした。その際、オリジナルの画像をメモリに保持するようにした。これは、モザイク処理を行う際、リサイズして画面にフィットして表示している画像(canvas_img)と、オリジナルサイズの画像(original_img)の両方に処理を行うためだ。

つまり、ウィンドウのキャンバス上でモザイクを掛けたとき、同時に、オリジナルサイズの画像の方にもモザイクを掛けておく。そして、保存ボタンを押した時には、オリジナルサイズの画像の方を保存する。そうすることで、高画質の画像を損なうことなくモザイク掛けした画像を作成できる。

そして、肝心のモザイク処理を行っているのが以下の部分となる。画像イメージ(PIL.Image)に対して、左上座標start_xyから右下座標end_xyまで、指定サイズのモザイクを掛けるプログラムとなっている。以下の二つ目の関数mosaic_x2の方は、画面上のキャンバスとオリジナルサイズの画像の両方にモザイクを掛けるものだ。

# モザイクを掛ける処理
def mosaic(img, start_xy, end_xy, size=MOSAIC_SIZE):
    """モザイク処理を行う関数"""
    x0, y0 = start_xy
    x1, y1 = end_xy
    region = img.crop((x0, y0, x1, y1))
    region = region.resize(
        ((x1 - x0) // int(size), (y1 - y0) // int(size)),
        Image.NEAREST
    )
    region = region.resize((x1 - x0, y1 - y0), Image.NEAREST)
    img.paste(region, (x0, y0, x1, y1))
    return img

def mosaic_x2(cv_img, org_img, start_xy, end_xy, size):
    """画面画像とオリジナル画像の二つにモザイク処理を行う関数"""
    # 画面画像にモザイクを掛ける
    cv_img = mosaic(cv_img, start_xy, end_xy, size=size)
    # 元画像にもモザイクを掛けておく
    sx0, sy0 = int(start_xy[0] * rate), int(start_xy[1] * rate)
    sx1, sy1 = int(end_xy[0] * rate), int(end_xy[1] * rate)
    org_img = mosaic(org_img, (sx0, sy0), (sx1, sy1), size=int(size * rate))
    return cv_img, org_img

次の部分を確認しよう。このプログラムでは、モザイクを掛けたい範囲をマウスでドラッグした時に、選択範囲の示す青い枠が表示され、マウスボタンを離した時にモザイク処理が掛かるようにしている。

このマウス処理を行うために、次のようにマウスイベントが発生するように設定する。以下のプログラムは、eg.Graph要素でキャンバスを作成し、キャンバスにマウスイベントをバインドしている部分だ。
# キャンバス作成
canvas = eg.Graph(key="-cv", canvas_size=CANVAS_SIZE) 
# メインウィンドウを作成
window = eg.Window("画像表示", layout=[
    [canvas],  # キャンバスを配置
    [eg.HSeparator()],
    [
        eg.Push(), eg.Text("モザイクサイズ:"),
        eg.Slider(range=(2, 50), key="-ms", orientation="h", default_value=MOSAIC_SIZE),
        eg.Button("保存"), eg.Button("終了")
    ],
])
# マウスイベントをバインド
canvas.bind_events({
    "<ButtonPress>": "mousedown",
    "<ButtonRelease>": "mouseup",
    "<Motion>": "mousemove"
}, "system")

なお、上記のように、bind_eventsメソッドを使うことで、イベントハンドラにて、イベントが発生するようになる。下記は、キャンバスを対象にしたマウスイベントを処理している部分だ。

# イベントループ
while window.is_alive():
    event, values = window.read()
    if event == "-cv":  # キャンバスのイベント
        handle_mouse_event(window, values)
…省略…
def handle_mouse_event(window, values):
    """マウスイベントを処理する関数"""
    global is_mouse_down, start_xy, move_list, canvas_img, original_img
    # マウスイベントの種類と位置を取得
    event_type = values["event_type"] if "event_type" in values else ""
    event = values["event"] if "event" in values else {"x": 0, "y": 0}
    if event_type == "mousedown":  # マウスボタンを押したとき
        is_mouse_down = True
        start_xy = (event.x, event.y)
    elif event_type == "mousemove" and is_mouse_down:  # マウスが動いたとき
        move_list.append((event.x, event.y))
        window.post_event_after(1, "@drawing", {})  # 1ms後に描画イベントを発生
    elif event_type == "mouseup":  # マウスボタンを離したとき
        is_mouse_down = False
        w, h = (event.x - start_xy[0], event.y - start_xy[1])
        if w < 0 or h < 0:
            return
        mosaic_size = values["-ms"] if "-ms" in values else MOSAIC_SIZE
        canvas_img, original_img = mosaic_x2(canvas_img, original_img, start_xy, (event.x, event.y), mosaic_size)
        window["-cv"].draw_image(location=(0, 0), data=canvas_img)
        move_list = []

ドラッグを処理するためのマウスイベントは、マウスボタンを押した時(→ mousedown)、ボタンを離した時(→ mouseup)、カーソルを移動した時(→ mousemove)の3つを処理することになる。

なお、マウスカーソルを動かした時のイベントは、大量に発生するので、その度に画面の再描画が行うと画面がカクカクしてしまう。と言うのもTkEasyGUIはそれほど描画性能が良いわけではない。

そのため、描画処理をスキップするために、デバウンスと呼ばれる処理(イベントの発火頻度を抑える仕組み)を行っている。マウスが移動するたびに、リスト変数move_listに座標を追加するのだが、move_listの全てを反映するのではなく、move_listの末尾にある座標のみを描画する仕組みにすることで、カクカク感が発生しないように工夫している。

まとめ

以上、今回は、モザイク掛けを行う専用ツールを作ってみた。当然、何でもできる高機能なツールも良いが、「一つしかできない」という機能を限定した専用ツールを作ることで、操作に迷うこともなく手軽に使えるツールにすることができる。Pythonを使うと、そうした専用ツールを手軽に作ることができる。専用ツールの作成は楽しいので、ぜひ挑戦してみよう。

自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ認定、2010年 OSS貢献者章受賞。これまで50冊以上の技術書を執筆した。直近では、「大規模言語モデルを使いこなすためのプロンプトエンジニアリングの教科書(マイナビ出版)」「Pythonでつくるデスクトップアプリ(ソシム)」「実践力を身につける Pythonの教科書 第2版」「シゴトがはかどる Python自動処理の教科書(マイナビ出版)」など。