見出し画像

Python tkinterを使ってOpenAIで画像を生成させるプログラム③

連続投稿になりますが縮小画像を表示するだけで保存も何もできないのは非常に片手落ちになるため1:1表示と画像の保存機能を追加しました。
1:1表示をするのはサブウィンドウを開いて表示する方法にしたいのですが、tkinterのサブウィンドウは何も考えずにApplicationクラスの中に実装するとコードが非常に煩雑になり可読性が落ちるのでImageDialogというクラスを作ることにしました。こんなコードです

# oagui_imagedialog.py

import tkinter as tk
from tkinter import filedialog
from PIL import Image, ImageTk
from io import BytesIO
import os

class ImageDialog:
    currentImage = None
    saveDirectory = "./"

    @staticmethod
    def show(root, image):
        ImageDialog.currentImage = image
        dlgImage = tk.Toplevel(root)
        dlgImage.title("AIが作った画像だよ!")
        dlgImage.geometry("1024x1024")
        can = tk.Canvas(dlgImage, bg="white", width=1024, height=1024)
        img = Image.open(BytesIO(image))
        can.photo = ImageTk.PhotoImage(img)
        can.create_image(0, 0, image=can.photo, anchor=tk.NW)
        can.bind("<Double-Button-1>", ImageDialog.__fileSave)
        can.pack()

        dlgImage.grab_set()
        dlgImage.focus_set()
        dlgImage.transient(root.master)

        root.master.wait_window(dlgImage)

    @staticmethod
    def __fileSave(event):
        filename = filedialog.asksaveasfilename(
            title="名前をつけて保存", 
            filetypes=[("PNG", "png")], 
            defaultextension="png",
            initialdir=ImageDialog.saveDirectory
        )
        if (len(filename) == 0): return
        ImageDialog.saveDirectory = os.path.dirname(filename)
        with open(filename, "wb") as f:
            f.write(ImageDialog.currentImage)

    

静的メソッドと静的変数だけで構成しました。
インスタンス化に意味がなさそうなので手軽に使えるユーティリティクラスみたいな実装になっています。
やることはたった1つ

ImageDialog.show(root, image)

と呼び出すだけでモーダルなサブウィンドウが表示されます。画面いっぱいに1024x1024の画像が表示され、画像をダブルクリックすると保存ダイアログが表示されるので好きな場所に好きな名前で保存できる仕様になっています。
rootは親ウィンドウのインスタンス、imageはOpenAIから取得した素のストリームです。

続いてメインウィンドウのコードも若干修正になりました。主にサブウィンドウ表示用のための改修です。

# oagui.py

import tkinter as tk
from tkinter import messagebox
from PIL import Image, ImageTk
from oacont import OpenAIController, OpenAIError
from io import BytesIO
from oagui_imagedialog import ImageDialog

OPENAI_API_KEY = "あなたのAPIキーを入力しちゃってください"

class Application(tk.Frame):
    NUMBER_OF_IMAGES = 4
    CANVAS_BASE_X = 22
    CANVAS_BASE_Y = 120
    CANVAS_EXT_X = 440
    CANVAS_EXT_Y = 536
    cans = {}
    originImages = {}

    def __init__(self,master = None):
        super().__init__(master)
        self.pack()

        master.geometry("860x960")
        master.title("OpenAiで画像生成しちゃうよん")
        self.oac = OpenAIController(OPENAI_API_KEY)
        self.__create_controls()
    
    def __create_canvases(self):
        i = 0
        while i < self.NUMBER_OF_IMAGES:
            can = tk.Canvas(self.master, bg="white", width=400, height=400)
            if i == 0: can.place(x=self.CANVAS_BASE_X, y=self.CANVAS_BASE_Y)
            elif i == 1: can.place(x=self.CANVAS_EXT_X, y=self.CANVAS_BASE_Y)
            elif i == 2: can.place(x=self.CANVAS_BASE_X, y=self.CANVAS_EXT_Y)
            else: can.place(x=self.CANVAS_EXT_X, y=self.CANVAS_EXT_Y)
            can.bind("<Button-1>", self.__canvas_Click)
            self.cans[i] = can
            i += 1

    def __create_controls(self):
        tk.Label(text="生成呪文").place(x=20, y=20)
        self.tbKeyword = tk.Text(width=92, height=6)
        self.tbKeyword.place(x=90, y=22)
        btnA = tk.Button(self.master, text="生成しちゃうぞ!",
                          command=self.__btnCreate_Click, width=12, height=5)
        btnA.place(x=750, y=18)
        self.__create_canvases()
    
    def __canvas_Click(self, event):
        if(len(self.originImages) == 0): return
        can = event.widget
        image = self.originImages[can]
        ImageDialog.show(self, image)
  
    def __btnCreate_Click(self):
        images = {}
        wd = self.tbKeyword.get("1.0", "end")
        if (len(wd) == 1): return
        try:
            images = self.oac.GenerateImages(wd, self.NUMBER_OF_IMAGES)
        except OpenAIError as e:
            messagebox.showerror("エラーっす", e)
            return
        self.__showGeneratedImages(images)
    
    def __showGeneratedImages(self, images):
        self.originImages.clear()
        i = 0
        while i < self.NUMBER_OF_IMAGES:
            origin = images[i]
            self.originImages[self.cans[i]] = origin

            img = Image.open(BytesIO(origin)).resize((400, 400))
            self.cans[i].photo = ImageTk.PhotoImage(img)
            self.cans[i].create_image(0, 0, image=self.cans[i].photo, anchor=tk.NW)
            i += 1
    
def main():
    win = tk.Tk()
    app = Application(master=win)
    app.mainloop()

if __name__ == "__main__":
    main()

前回からの大きな違いは__canvas_Clickメソッドの追加です。
画面上の4つのサムネイルのうちどれかを左クリックするとImageDialog.show(self, image)によってサブウィンドウが開いて画像を1:1表示します。
OpenAIから取得した最新の生画像データはoriginImagesディクショナリーに保存させるように変更しました。クリックされたキャンバスがキーになっているのでディクショナリーから生画像を取り出してImageDialog.showメソッドに引き渡しています。
最後にOpenAIControllerクラスですが、ImageTkのImageオブジェクトではなくOpenAIから取得した素のストリーミングのディクショナリーを返すように仕様変更してあります。これはファイル保存しやすくするための措置です。

# oacont.py

import openai
import base64

class OpenAIController:
    def __init__(self, apiKey):
        openai.api_key = apiKey
    
    def __generateRequestWord(self, word):
        requestWord = "次の原文を前書きや挨拶など余分な出力なしで、画像生成AIに素晴らしい画像を出力させるための指示文章を英語で出力してください。原文「"
        requestWord = requestWord + word + "」"
        try:
            completion = openai.ChatCompletion.create(
                model="gpt-3.5-turbo", 
                messages=[{"role": "user", "content": requestWord}]
            )
            return completion.choices[0].message.content
        except openai.error.RateLimitError as e: raise

    def GenerateImages(self, word, numberOfImages):
        results = {}
        try:
            requestWord = self.__generateRequestWord(word)
            response = openai.Image.create(
                prompt = requestWord,
                n = numberOfImages,
                size = "1024x1024",
                response_format = "b64_json",
            )

            for data, n in zip(response["data"], range(numberOfImages)):
                img_data = base64.b64decode(data["b64_json"])
                results[n] = img_data
            return results
        except openai.error.RateLimitError as e: raise OpenAIError(f"OpenAI API request exceeded rate limit: {e}")

class OpenAIError(Exception):
    pass

まとめ

tkinterは非常に使い勝手の良いGUIライブラリーですが、メインウィンドウとサブウィンドウの違いが多すぎてWin32なプログラムしか知らない私にとっては非常にとっつき難い仕様となっています。
サブウィンドウの実装は別クラスにしてすっきりさせることが出来たと思います。
また、今回画像をクリップボードにコピーする機能を追加しようとしたのですが、どうやらOSごとで対応が異なるようなのでやめておきました。とりあえず保存出来るようになったのでAIに生成してもらったお気に入りの画像を簡単に保存できるようにはなりました。
Python超入門して3日目ですが、まだまだ未知の領域が多そうなので引き続き精進して参ります。多分。

画像の保存機能

この記事が気に入ったらサポートをしてみませんか?