2020年12月6日日曜日

Python+Pillowでトイカメラ風に周辺光量落ちさせる その2

最近、お出かけすることが減って、Pythonを勉強しています。
そのPython言語の、Pillowという画像ライブラリを使ったスクリプトです。
Pythonをインストールして、Pillowのライブラリをpipでインストールしておく必要があります。
記事を書いている時点のPythonは3.8です。

前回のトイカメラ風に加工するスクリプトを、TkinterというGUIライブラリを使って、
画面で操作できるようにしました。
GUI化することで、効果を確認しながらパラメータを決められるのが良いですが、
何枚も同じ加工を連続して行うのは向きません。

以下にスクリプトのコードを書きます。



"""
トイカメラ風に周辺減光させる 
tkinterを使ったGUI版
Usage : コマンドラインから以下のコマンドで起動
    python toycamera_gui.py
"""

import os
from PIL import Image, ImageDraw, ImageFilter, ImageTk
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog, messagebox

class FormClass:
    """ 
    Toycamera風に加工する画面のフォームを作成するクラス
    """

    def __init__(self, root):
        # インスタンス変数に初期値設定
        self.root = root     # Windowオブジェクト
        self.readimg = None  # 入力画像
        self.dst_img = None  # 加工結果画像

        rowno = 0            # 表示位置(行番号)

        # ラベルを作成
        tk.Label(root, text='入力ファイルPATH'
                ).grid(column=0, row=rowno)

        # 入力ファイル名
        self.src_path = tk.StringVar()

        # テキストボックスを作成
        tb_widget = tk.Entry(root, 
                             width=120, 
                             textvariable=self.src_path)
        tb_widget.grid(column=1, row=rowno)

        # 入力ファイル選択ボタンを作成
        btn_widget = tk.Button(root, 
                               text='入力ファイル選択', 
                               command=self.select_file)
        btn_widget.grid(column=2, row=rowno)
        rowno += 1

        # 塗り残す範囲の比率 スケールの作成
        tk.Label(root, text='塗り残す範囲の比率(%)'
                ).grid(column=0, row=rowno)

        self.rate = tk.DoubleVar()
        rate_sc = tk.Scale(
                     root,
                     variable=self.rate,
                     orient=tk.HORIZONTAL,
                     from_=0.0,
                     to=100,
                     command=self.draw_effect)
        rate_sc.grid(column=1, 
                     row=rowno, 
                     sticky=(tk.N, tk.E, tk.S, tk.W))
        self.rate.set(40)
        rowno += 1

        # ぼかし半径 スケールの作成
        tk.Label(root, text='ぼかし半径'
                ).grid(column=0, row=rowno)

        self.blur_radius = tk.IntVar()
        blur_sc = tk.Scale(
                     root,
                     variable=self.blur_radius,
                     orient=tk.HORIZONTAL,
                     from_=0,
                     to=40.0,
                     command=self.draw_effect)
        blur_sc.grid(column=1, 
                     row=rowno, 
                     sticky=(tk.N, tk.E, tk.S, tk.W))
        self.blur_radius.set(20)
        rowno += 1

        # 効果の強さ スケールの作成
        tk.Label(root, text='効果の強さ'
                ).grid(column=0, row=rowno)

        self.strength = tk.IntVar()
        strength_sc = tk.Scale(
                     root,
                     variable=self.strength,
                     orient=tk.HORIZONTAL,
                     from_=0,
                     to=255,
                     command=self.draw_effect)
        strength_sc.grid(column=1, 
                         row=rowno, 
                         sticky=(tk.N, tk.E, tk.S, tk.W))
        self.strength.set(80)
        rowno += 1

        # 塗りつぶし色 コンボボックスの作成
        tk.Label(root, text='塗りつぶし色'
                ).grid(column=0, row=rowno)

        self.select_color = tk.StringVar()
        color_combo = ttk.Combobox(
                     root,
                     textvariable=self.select_color)
        color_combo.grid(column=1, 
                         row=rowno, 
                         sticky=(tk.N, tk.W))
        color_combo['values'] = ('白', '黒')
        color_combo.current(1)
        color_combo.bind("<>", self.draw_effect)
        rowno += 1

        # ファイル保存ボタンを作成
        savebtn_widget = tk.Button(root, 
                                   text='ファイル保存', 
                                   command=self.show_save_dialog)
        savebtn_widget.grid(column=0, row=rowno,
                            sticky=(tk.N))

        self.CANVAS_WIDTH = 800
        self.CANVAS_HEIGHT = 600

        # 画像を表示するためのキャンバスの作成
        self.canvas = tk.Canvas(self.root, 
                                width=self.CANVAS_WIDTH, 
                                height=self.CANVAS_HEIGHT)
        self.canvas.grid(column=1, row=rowno, columnspan=2)

    # 入力ファイル選択ボタン押下イベント
    def select_file(self):
        # ファイル選択ダイアログを表示
        ftypes = [("All Files", ".*"),
                  ("JPEG Image Files", ".jpg .jpeg"),
                  ("PNG Image Files", ".png")]

        filepath = self.src_path.get()
        if len(filepath) == 0:
            idir = os.path.abspath(os.path.dirname(__file__))
        else:
            idir = os.path.dirname(filepath)
        filepath = filedialog.askopenfilename(filetypes = ftypes,
                                              initialdir = idir)
        if len(filepath) == 0 :
            return
        self.src_path.set(filepath)

        # 画像を読み取り
        self.readimg = Image.open(filepath)

        # 加工した画像を表示
        self.draw_effect()

    # 加工した画像を表示する
    def draw_effect(self,val=0):
        if self.readimg is None :
            return

        # 塗りつぶし色
        fill_color = None
        if self.select_color.get() == '白' :
            fill_color = (255, 255, 255)
        else :
            # 黒く塗りつぶす場合
            fill_color = (0, 0, 0)

        # 円を描画する開始位置、終了位置を計算する
        w,h = self.readimg.size
        radius = max(w,h) * self.rate.get() / 100.0
        w_start = (w / 2) - radius
        w_end = (w / 2) + radius
        h_start = (h / 2) - radius
        h_end = (h / 2) + radius

        # マスク画像を描画する
        mask = Image.new("L", (w, h), 255-self.strength.get())
        draw = ImageDraw.Draw(mask)
        draw.ellipse((w_start, h_start, w_end, h_end), fill=255)
        mask_blur = mask.filter(
                        ImageFilter.GaussianBlur(
                            self.blur_radius.get()))

        # 塗りつぶし画像を作成する
        plane = Image.new("RGB", (w, h), fill_color)

        # 元の画像と、塗りつぶし画像を合成する
        self.dst_img = Image.composite(self.readimg, plane, mask_blur)

        # 画像の表示倍率を計算する
        rate = min(1.0, 
                   self.CANVAS_WIDTH / self.readimg.width, 
                   self.CANVAS_HEIGHT / self.readimg.height)

        # 表示用に画像を縮小する
        new_w = int(self.readimg.width * rate)
        new_h = int(self.readimg.height * rate)
        small_resized = self.dst_img.resize((new_w, new_h))

        # キャンバスに画像を表示する。
        self.im = ImageTk.PhotoImage(image=small_resized)
        self.canvas.create_image(0, 0, anchor=tk.NW, image=self.im)

    # ファイル保存ボタンイベント
    def show_save_dialog(self):
        if self.dst_img is not None :
            ftypes = [("All Files", ".*"),
                      ("JPEG Image Files", ".jpg .jpeg"),
                      ("PNG Image Files", ".png")]

            ini_fname = os.path.basename(self.src_path.get())
            filename = filedialog.asksaveasfilename(filetypes=ftypes,
                                                    initialfile=ini_fname)
            if filename:
                # 保存先ファイルが指定されたら、加工した画像を保存
                self.dst_img.save(filename, quality=100)
        else :
            tk.messagebox.showerror(title="エラー", 
                                    message="入力ファイルを指定してください")

# windowを描画
window = tk.Tk()
# windowサイズを変更
window.geometry("1000x800")
# windowタイトルを設定
window.title("Toycamera effect")

# フォームを作成、表示
FormClass(window)

# 画面を操作されるのを待つ
window.mainloop()


上のスクリプトをUTF-8のエンコーディングで、toycamera_gui.py というファイルに保存します。
Windowsだとコマンドプロンプト等を開いて、スクリプトを保存したフォルダにcdコマンドで移動して、

python toycamera_gui.py

で実行します。 実行すると、



このような画面を表示します。
入力ファイル選択ボタンで、ファイル選択ダイアログを開きます。
そこでファイルを選択すると、




このように画像のプレビューを表示します。

「塗り残す範囲の比率」を大きくすると、塗りつぶしする面積が狭くなります。

「ぼかし半径」を大きくすると、塗りつぶしの境界がぼけます。

「効果の強さ」を大きくすると、塗りつぶしがはっきりします。

「塗りつぶし色」を白にすると、周辺を白く塗りつぶしします。

「ファイル保存ボタン」をクリックすると、保存先選択ダイアログを開きます。


塗りつぶしを白に変更した例



0 件のコメント:

コメントを投稿