ちょっと話題の記事

【ChatGPT】GPT-4でPythonの画像ビューワを作成してみた

2023.03.15

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

新規事業統括部の山本です。

今日OpenAIのChatGPTのモデルとして、GPT-4が利用可能になりました。早速使ってみようと思います。

やってみる

今回は画像のビューワを作成してみます。ちょうどデータセットの画像や、画像モデルに入力した結果を表示するツールがほしいと思っていました。

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

def browse_folder():
    folder_path = filedialog.askdirectory()
    if not folder_path:
        return

    images_frame.delete("all")
    load_images(folder_path)

def load_images(folder_path):
    supported_extensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff"]
    for file in os.listdir(folder_path):
        if os.path.splitext(file)[-1].lower() in supported_extensions:
            file_path = os.path.join(folder_path, file)
            display_image(file_path)

def display_image(file_path):
    image = Image.open(file_path)
    image.thumbnail((100, 100))
    tk_image = ImageTk.PhotoImage(image)

    image_label = tk.Label(images_frame, image=tk_image)
    image_label.image = tk_image
    image_label.pack(side=tk.LEFT, padx=5, pady=5)

root = tk.Tk()
root.title("画像ビューワ")

browse_button = tk.Button(root, text="フォルダを選択", command=browse_folder)
browse_button.pack(padx=10, pady=10)

images_frame = tk.Canvas(root)
images_frame.pack()

root.mainloop()

実際に動かしてみると、画像が一列に表示がされました。

かなり曖昧な表現でも、対応してくれました。また、コード以外にも動作についての説明もしてくれました。(自分ではコード書かなくても、動作するプログラムが生成されたのは素晴らしいですね!)

修正する

上のプログラムの動作は指示したとおりですが、やはりタイル状に表示してほしいところです。以下のようにお願いしてみました。

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

def browse_folder():
    folder_path = filedialog.askdirectory()
    if not folder_path:
        return

    images_frame.delete("all")
    load_images(folder_path)

def load_images(folder_path):
    global image_paths
    supported_extensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff"]
    image_paths = [os.path.join(folder_path, file) for file in os.listdir(folder_path) if os.path.splitext(file)[-1].lower() in supported_extensions]
    display_images(0)

def display_images(start_index):
    global current_start_index
    current_start_index = start_index

    images_frame.delete("all")
    for i in range(start_index, min(start_index + 100, len(image_paths))):
        file_path = image_paths[i]
        image = Image.open(file_path)
        image.thumbnail((100, 100))
        tk_image = ImageTk.PhotoImage(image)

        image_label = tk.Label(images_frame, image=tk_image)
        image_label.image = tk_image
        image_label.grid(row=i // 10, column=i % 10, padx=5, pady=5)

def prev_images():
    global current_start_index
    new_start_index = max(current_start_index - 100, 0)
    display_images(new_start_index)

def next_images():
    global current_start_index
    new_start_index = min(current_start_index + 100, len(image_paths) - 1)
    display_images(new_start_index)

root = tk.Tk()
root.title("画像ビューワ")

current_start_index = 0
image_paths = []

browse_button = tk.Button(root, text="フォルダを選択", command=browse_folder)
browse_button.pack(padx=10, pady=10)

prev_button = tk.Button(root, text="前へ", command=prev_images)
prev_button.pack(side=tk.LEFT, padx=10, pady=10)

next_button = tk.Button(root, text="次へ", command=next_images)
next_button.pack(side=tk.RIGHT, padx=10, pady=10)

images_frame = tk.Canvas(root)
images_frame.pack()

root.mainloop()

このコードを動かすと、以下のようになりました。

ただ「次へ」のボタンを押すと、下に追加されてしまいました。

さらに修正する

次へを押したときは、次のページの画像のみ表示されてほしいので、以下のようにお願いしてみました。

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

def browse_folder():
    folder_path = filedialog.askdirectory()
    if not folder_path:
        return

    clear_images()
    load_images(folder_path)

def load_images(folder_path):
    global image_paths
    supported_extensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff"]
    image_paths = [os.path.join(folder_path, file) for file in os.listdir(folder_path) if os.path.splitext(file)[-1].lower() in supported_extensions]
    display_images(0)

def display_images(start_index):
    global current_start_index
    current_start_index = start_index

    clear_images()

    for i in range(start_index, min(start_index + 100, len(image_paths))):
        file_path = image_paths[i]
        image = Image.open(file_path)
        image.thumbnail((100, 100))
        tk_image = ImageTk.PhotoImage(image)

        image_label = tk.Label(images_frame, image=tk_image)
        image_label.image = tk_image
        image_label.grid(row=i // 10, column=i % 10, padx=5, pady=5)

def clear_images():
    for widget in images_frame.winfo_children():
        widget.grid_forget()

def prev_images():
    global current_start_index
    new_start_index = max(current_start_index - 100, 0)
    display_images(new_start_index)

def next_images():
    global current_start_index
    new_start_index = min(current_start_index + 100, len(image_paths) - 1)
    display_images(new_start_index)

root = tk.Tk()
root.title("画像ビューワ")

current_start_index = 0
image_paths = []

browse_button = tk.Button(root, text="フォルダを選択", command=browse_folder)
browse_button.pack(padx=10, pady=10)

prev_button = tk.Button(root, text="前へ", command=prev_images)
prev_button.pack(side=tk.LEFT, padx=10, pady=10)

next_button = tk.Button(root, text="次へ", command=next_images)
next_button.pack(side=tk.RIGHT, padx=10, pady=10)

images_frame = tk.Frame(root)
images_frame.pack()

root.mainloop()

実行すると、以下のようになりました。(良い感じですね!)

機能を追加してみる

画像を見るだけなら上記で十分ですが、ツールとしては何かの結果を表示しながら比較したところです。そのためは別ウィンドウでボタンやスライダーを作成し、それに応じて表示を切り替えるようにしたいです。

今回は、UIやコールバック部分のコードを生成してもらうために、ボタンを作成しモノクロにするようにお願いしてみました。

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

def browse_folder():
    folder_path = filedialog.askdirectory()
    if not folder_path:
        return

    clear_images()
    load_images(folder_path)

def load_images(folder_path):
    global image_paths
    supported_extensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff"]
    image_paths = [os.path.join(folder_path, file) for file in os.listdir(folder_path) if os.path.splitext(file)[-1].lower() in supported_extensions]
    display_images(0)

def display_images(start_index):
    global current_start_index
    current_start_index = start_index

    clear_images()

    for i in range(start_index, min(start_index + 100, len(image_paths))):
        file_path = image_paths[i]
        image = Image.open(file_path)
        if grayscale_var.get():
            image = image.convert("L")
        image.thumbnail((100, 100))
        tk_image = ImageTk.PhotoImage(image)

        image_label = tk.Label(images_frame, image=tk_image)
        image_label.image = tk_image
        image_label.grid(row=i // 10, column=i % 10, padx=5, pady=5)

def clear_images():
    for widget in images_frame.winfo_children():
        widget.grid_forget()

def prev_images():
    global current_start_index
    new_start_index = max(current_start_index - 100, 0)
    display_images(new_start_index)

def next_images():
    global current_start_index
    new_start_index = min(current_start_index + 100, len(image_paths) - 1)
    display_images(new_start_index)

def toggle_grayscale():
    display_images(current_start_index)

root = tk.Tk()
root.title("画像ビューワ")

current_start_index = 0
image_paths = []

browse_button = tk.Button(root, text="フォルダを選択", command=browse_folder)
browse_button.pack(padx=10, pady=10)

prev_button = tk.Button(root, text="前へ", command=prev_images)
prev_button.pack(side=tk.LEFT, padx=10, pady=10)

next_button = tk.Button(root, text="次へ", command=next_images)
next_button.pack(side=tk.RIGHT, padx=10, pady=10)

images_frame = tk.Frame(root)
images_frame.pack()

settings_window = tk.Toplevel(root)
settings_window.title("設定")

grayscale_var = tk.BooleanVar()
grayscale_toggle = tk.Checkbutton(settings_window, text="モノクロ表示", variable=grayscale_var, command=toggle_grayscale)
grayscale_toggle.pack(padx=10, pady=10)

root.mainloop()

実行してみると、以下のようになりました。トグルボタンではありませんが、チェックボックスをクリックすると、モノクロ・カラーが切り替わって表示されるようになりました。

ベースのコードとしては、かなり良いものができたように思えます。これを真似して、色々なビューワツールが作成できそうです。また、さらに欲しい機能ができたら、再度ChatGPTにお願いすることもできそうです。

型定義を加えてもらう

型アノテーションの無いPythonは扱いにくいので、型定義を加えておいてもらいましょう。

import os
from typing import List
import tkinter as tk
from tkinter import filedialog
from PIL import Image, ImageTk

def browse_folder() -> None:
    folder_path = filedialog.askdirectory()
    if not folder_path:
        return

    clear_images()
    load_images(folder_path)

def load_images(folder_path: str) -> None:
    global image_paths
    supported_extensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff"]
    image_paths = [os.path.join(folder_path, file) for file in os.listdir(folder_path) if os.path.splitext(file)[-1].lower() in supported_extensions]
    display_images(0)

def display_images(start_index: int) -> None:
    global current_start_index
    current_start_index = start_index

    clear_images()

    for i in range(start_index, min(start_index + 100, len(image_paths))):
        file_path = image_paths[i]
        image = Image.open(file_path)
        if grayscale_var.get():
            image = image.convert("L")
        image.thumbnail((100, 100))
        tk_image = ImageTk.PhotoImage(image)

        image_label = tk.Label(images_frame, image=tk_image)
        image_label.image = tk_image
        image_label.grid(row=i // 10, column=i % 10, padx=5, pady=5)

def clear_images() -> None:
    for widget in images_frame.winfo_children():
        widget.grid_forget()

def prev_images() -> None:
    global current_start_index
    new_start_index = max(current_start_index - 100, 0)
    display_images(new_start_index)

def next_images() -> None:
    global current_start_index
    new_start_index = min(current_start_index + 100, len(image_paths) - 1)
    display_images(new_start_index)

def toggle_grayscale() -> None:
    display_images(current_start_index)

root = tk.Tk()
root.title("画像ビューワ")

current_start_index: int = 0
image_paths: List[str] = []

browse_button = tk.Button(root, text="フォルダを選択", command=browse_folder)
browse_button.pack(padx=10, pady=10)

prev_button = tk.Button(root, text="前へ", command=prev_images)
prev_button.pack(side=tk.LEFT, padx=10, pady=10)

next_button = tk.Button(root, text="次へ", command=next_images)
next_button.pack(side=tk.RIGHT, padx=10, pady=10)

images_frame = tk.Frame(root)
images_frame.pack()

settings_window = tk.Toplevel(root)
settings_window.title("設定")

grayscale_var = tk.BooleanVar()
grayscale_toggle = tk.Checkbutton(settings_window, text="モノクロ表示", variable=grayscale_var, command=toggle_grayscale)
grayscale_toggle.pack(padx=10, pady=10)

root.mainloop()

(補足:まとめて実行すると)

新しくChatを作成し、今までの指示をまとめて渡してみました。

import os
import tkinter as tk
from itertools import islice
from tkinter import filedialog

from PIL import Image, ImageOps, ImageTk

class ImageGallery:
    def __init__(self, master):
        self.master = master
        self.master.title("画像ビューワ")

        self.image_canvas = tk.Canvas(self.master, width=800, height=800)
        self.image_canvas.pack()

        self.toggle_button = tk.Button(self.master, text="モノクロ切り替え", command=self.toggle_monochrome)
        self.toggle_button.pack()

        self.next_button = tk.Button(self.master, text="次へ", command=self.next_page)
        self.next_button.pack(side=tk.RIGHT)

        self.prev_button = tk.Button(self.master, text="前へ", command=self.prev_page)
        self.prev_button.pack(side=tk.RIGHT)

        self.monochrome = False
        self.current_page = 0
        self.images_per_page = 100
        self.images = []
        self.folder_path = ""

    def load_images(self):
        self.folder_path = filedialog.askdirectory()
        if not self.folder_path:
            return

        self.images = []
        for filename in os.listdir(self.folder_path):
            if filename.lower().endswith((".png", ".jpg", ".jpeg", ".gif", ".bmp")):
                self.images.append(os.path.join(self.folder_path, filename))

        self.current_page = 0
        self.display_images()

    def display_images(self):
        self.image_canvas.delete("all")
        x = 0
        y = 0
        max_width = self.image_canvas.winfo_width()
        max_height = self.image_canvas.winfo_height()

        for image_path in islice(
            self.images, self.current_page * self.images_per_page, (self.current_page + 1) * self.images_per_page
        ):
            img = Image.open(image_path)
            img.thumbnail((max_width // 10, max_height // 10))

            if self.monochrome:
                img = ImageOps.grayscale(img)

            tk_img = ImageTk.PhotoImage(img)
            self.image_canvas.create_image(x, y, anchor=tk.NW, image=tk_img)
            self.image_canvas.image = tk_img
            x += img.width

            if x >= max_width:
                x = 0
                y += img.height

    def next_page(self):
        if (self.current_page + 1) * self.images_per_page < len(self.images):
            self.current_page += 1
            self.display_images()

    def prev_page(self):
        if self.current_page > 0:
            self.current_page -= 1
            self.display_images()

    def toggle_monochrome(self):
        self.monochrome = not self.monochrome
        self.display_images()

def main():
    root = tk.Tk()
    app = ImageGallery(root)
    app.load_images()
    root.mainloop()

if __name__ == "__main__":
    main()

実行してみると、以下のようなウィンドウが表示されました。(意図した動作とは、異なる感じですね)

まとめ・感想

GPT-4を利用したChatGPTで、Pythonプログラムの画像ビューワを作成しました。自分でコードを書かずに、欲しいプログラムのベース部分を作成することができ、大幅に作業効率を上げることができそうです。また、プログラムを実行しながら、自分がほしいと思っている機能を考えながら順次付け加えていくことができ、非常に便利だと思いました。