画像入力で楽々！ChatGPT-4V と Python GUI 開発

# 1. はじめに

　ついに ChatGPT-4V が使えるようになりました！

　これまでGUIを作成するときには、ChatGPT に 
「まず左上にテキストボックスがあって、その右にボタンがあって...」
とちまちま指示する必要があったのですが、GPT-4V により画像を渡せるようになります。

この記事では、ChatGPT-4V を使うと GUI 作成がどのくらい楽になるのかを検証してみたいと思います！

作成するGUIはこちらのものです。

https://qiita.com/key353/items/e59927dcf20916f9d4ac

![main.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/cc396a8a-54fc-ea47-17a1-e2ff7dbfdaa7.png)

# 2. GPT-4V の読み取り精度の確認

## 2.1. 日本語のポンチ絵

　まず、元の記事で使用した日本語のポンチ絵を渡して、どのように読み取れるのかを確認します。
![スクリーンショット 2023-10-16 080824.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/c751fafa-d490-39eb-f424-0f905d58a89d.png)
GUIの各要素の位置はなんとなく読み取れている感じがしますが、日本語の読み取り精度はあまり高くないようです。

## 2.2. 英語のポンチ絵

　先ほどのポンチ絵を英語に変換してみます（左下だけ詳しく書き直しました）。
![スクリーンショット 2023-10-16 081202.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/14c1d0cb-6eab-cd0c-b833-e12afdb384dc.png)
各要素を正確に読み取れているのに加えて、どのようなGUIなのか機能も推測してくれています。
この精度で読み取ってくれるのは素晴らしいですね！

## 2.3. フレームの認識

　ポンチ絵中の破線はGUIのフレーム（各要素をグルーピングしたもの）ですが、破線は読み取りが難しいようで、英語にしてもなかなかうまく認識してくれませんでした。

そのため、以下のようにフレームごとにポンチ絵に色をつけて渡すようにしました。
![sample_design2_en_color.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/ccc00a8b-403f-b078-22ab-97ea9ee7cec7.png)
最終的にGPT-4Vで読み取った結果は以下のものです。
![スクリーンショット 2023-10-16 081911.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/8a7748bc-eb6f-8d5f-9a59-4ebc596c9829.png)

緑のフレームとピンクのフレームの親子構造が正しく認識できていないようですが、大きく影響はしないので、このまま進みます。

# 3. tkinterを使ったGUIアプリ

## 3.1. はじめに作成した GUI

　上記でポンチ絵から正しく各要素を読み取ることができたので、ここからPythonのコードを作成してもらいます。

![スクリーンショット 2023-10-16 082029.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/6d5d4eab-5d25-ea60-c1ac-d199c7cf4366.png)

```python
import tkinter as tk
from tkinter import filedialog, ttk
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

def select_file_path(entry):
    """ファイル選択ダイアログを開き、選択されたファイルのパスをエントリボックスに設定する"""
    filepath = filedialog.askopenfilename(title="CSVファイルを選択してください", filetypes=[("CSV files", "*.csv")])
    entry.delete(0, tk.END)  # エントリボックスの内容をクリア
    entry.insert(0, filepath)  # 新しいパスをエントリボックスに設定

def plot_graph(entry, combobox, scale, canvas):
    """CSVデータを読み込み、指定された線のスタイルでグラフを描画する"""
    filepath = entry.get()
    df = pd.read_csv(filepath)
    
    # 線のスタイルと太さを取得
    line_style = combobox.get()
    line_width = scale.get()
    
    # プロット
    fig, ax = plt.subplots(figsize=(5, 4))
    for column in df.columns[1:]:
        df.plot(x=df.columns[0], y=column, kind="line", ax=ax, linestyle=line_style, linewidth=line_width)

    # 既存のキャンバスオブジェクトに新しい図を描画
    canvas.figure = fig
    canvas.draw()

def save_image(canvas):
    """キャンバス上のグラフを画像として保存する"""
    canvas.figure.savefig("saved_graph.png")

def main():
    # メインウィンドウの作成
    root = tk.Tk()
    root.title("CSV Graph Plotter")

    # 上部フレーム (青色のフレーム)
    frame_top = tk.Frame(root, bg="lightblue", padx=5, pady=5)
    frame_top.pack(pady=20)

    entry_filepath = tk.Entry(frame_top, width=30)
    entry_filepath.pack(side=tk.LEFT, padx=5)
    btn_select_file = tk.Button(frame_top, text="Select file", command=lambda: select_file_path(entry_filepath))
    btn_select_file.pack(side=tk.LEFT, padx=5)
    btn_open_file = tk.Button(frame_top, text="Open file", command=lambda: plot_graph(entry_filepath, combobox_linetype, scale_linewidth, canvas))
    btn_open_file.pack(side=tk.LEFT, padx=5)

    # 左側フレーム (ピンク色のフレーム)
    frame_left = tk.Frame(root, bg="pink", padx=5, pady=5)
    frame_left.pack(side=tk.LEFT, padx=20, pady=20)

    label_linetype = tk.Label(frame_left, text="Linetype", bg="pink")
    label_linetype.pack(pady=5)
    combobox_linetype = ttk.Combobox(frame_left, values=["-", "--", "-.", ":"], state="readonly")
    combobox_linetype.pack(pady=5)
    combobox_linetype.set("-")

    label_linewidth = tk.Label(frame_left, text="Line width", bg="pink")
    label_linewidth.pack(pady=5)
    scale_linewidth = tk.Scale(frame_left, from_=1, to=5, orient=tk.HORIZONTAL, bg="pink")
    scale_linewidth.set(2)
    scale_linewidth.pack(pady=5)

    # 右側フレーム (緑色のフレーム)
    frame_right = tk.Frame(root, bg="lightgreen", padx=5, pady=5)
    frame_right.pack(side=tk.LEFT, padx=20, pady=20)

    canvas = FigureCanvasTkAgg(plt.figure(), master=frame_right)
    canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
    btn_save_image = tk.Button(frame_right, text="Save image", command=lambda: save_image(canvas))
    btn_save_image.pack(pady=20)

    root.mainloop()

if __name__ == "__main__":
    main()
```

* 実行時に1箇所だけエラーが出た（plot_graph関数のキャンバス表示）ので、ChatGPTに修正してもらいました。

実行すると以下のようになります。

![app2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/321412ba-6c85-0203-07de-824339e45d8a.gif)

元となったGUIと微妙に動作は異なりますが、ポンチ絵に書いた指示通りの動作をしています。

ただ、ポンチ絵とまったく同じデザインに作ることはできないようで、デザインが古臭いのが気になります...

## 3.2. ちょっとおしゃれに改修する

　デザインが気になるので、ChatGPTに改修をお願いします。
（ポンチ絵に矢印で注釈を加えたりしてみたり、もっと詳しくお願いしたりしてみたのですが、雑に投げる方がクオリティが高かったので、雑にお願いしています。）
![chat01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/14891536-70f6-7744-4a77-88867d60f1b5.png)

<details><summary>:page_facing_up: コード（折りたたんでいます）</summary><div>

```python
import tkinter as tk
from tkinter import filedialog, ttk
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

def select_file_path(entry):
    """ファイル選択ダイアログを開き、選択されたファイルのパスをエントリボックスに設定する"""
    filepath = filedialog.askopenfilename(title="CSVファイルを選択してください", filetypes=[("CSV files", "*.csv")])
    entry.delete(0, tk.END)  # エントリボックスの内容をクリア
    entry.insert(0, filepath)  # 新しいパスをエントリボックスに設定

def plot_graph(entry, combobox, scale, canvas, frame_right):
    """CSVデータを読み込み、指定された線のスタイルでグラフを描画する"""
    filepath = entry.get()
    df = pd.read_csv(filepath)
    
    # 線のスタイルと太さを取得
    line_style = combobox.get()
    line_width = scale.get()

    # 既存のキャンバスオブジェクトの図をクリア
    plt.close('all')
    
    # フレームのサイズからFigureのサイズを計算
    fig_width = frame_right.winfo_width() / canvas.figure.dpi
    fig_height = (frame_right.winfo_height() - 60) / canvas.figure.dpi  # 50を引くのは"Save image"ボタンの分

    # プロット
    fig, ax = plt.subplots(figsize=(fig_width, fig_height))
    for column in df.columns[1:]:
        df.plot(x=df.columns[0], y=column, kind="line", ax=ax, linestyle=line_style, linewidth=line_width)

    # 既存のキャンバスオブジェクトに新しい図を描画
    canvas.figure = fig
    canvas.draw()
    
def save_image(canvas):
    """キャンバス上のグラフを画像として保存する"""
    canvas.figure.savefig("saved_graph.png")

def main():
    # ウィンドウ作成
    root = tk.Tk()
    root.title("CSV Graph Plotter")
    
    # フレームの作成と配置
    frame_top = tk.Frame(root, bg='lightgray', padx=10, pady=10)
    frame_left = tk.Frame(root, bg='pink', padx=10, pady=10)
    frame_right = tk.Frame(root, bg='lightgreen', padx=10, pady=10)

    frame_top.pack(fill=tk.X, side=tk.TOP, padx=5, pady=5)
    frame_left.pack(fill=tk.Y, side=tk.LEFT, padx=5, pady=5)
    frame_right.pack(fill=tk.BOTH, expand=True, side=tk.RIGHT, padx=5, pady=5)

    entry_filepath = tk.Entry(frame_top, width=30)
    entry_filepath.pack(fill=tk.X, side=tk.LEFT, padx=5)  # fill=tk.X を追加
    btn_select_file = tk.Button(frame_top, text="Select file", command=lambda: select_file_path(entry_filepath))
    btn_select_file.pack(side=tk.LEFT, padx=5)
    btn_open_file = tk.Button(frame_top, text="Open file", command=lambda: plot_graph(entry_filepath, combobox_linetype, scale_linewidth, canvas, frame_right))
    btn_open_file.pack(side=tk.LEFT, padx=5)

    # 左側フレーム (ピンク色のフレーム)

    label_linetype = tk.Label(frame_left, text="Linetype", bg="pink")
    label_linetype.pack(pady=5)
    combobox_linetype = ttk.Combobox(frame_left, values=["-", "--", "-.", ":"], state="readonly")
    combobox_linetype.pack(pady=5)
    combobox_linetype.set("-")

    label_linewidth = tk.Label(frame_left, text="Line width", bg="pink")
    label_linewidth.pack(pady=5)
    scale_linewidth = tk.Scale(frame_left, from_=1, to=5, orient=tk.HORIZONTAL, bg="pink")
    scale_linewidth.set(2)
    scale_linewidth.pack(pady=5)

    # 右側フレーム (緑色のフレーム)
    btn_save_image = tk.Button(frame_right, text="Save image", command=lambda: save_image(canvas))
    btn_save_image.pack(pady=10, anchor='s')
    canvas = FigureCanvasTkAgg(plt.figure(), master=frame_right)
    canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

    root.mainloop()

if __name__ == "__main__":
    main()
```

</div></details>

追加で修正を依頼した点

* GUIをリサイズしたときに Save image ボタンがグラフに隠れてしまうため、対応を依頼しました。そのためにボタンが上に表示されるようになりました。
* グラフを再描画したときに、前のグラフが後ろに残っていたので、削除するように依頼しました。

これを実行すると以下のようになりました。
![スクリーンショット 2023-10-16 083932.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/d4279afb-9952-538e-be6b-524a8647440a.png)

ポンチ絵の指示とイコールではないですが、かなりデザインが良くなりました！

# 4. CustomTkinter で GUI を作成する

　最後に、GUIをおしゃれにするために、使用するライブラリを Python 標準の tkinter ライブラリから CustomTkinter ライブラリに変更してもらいます。
![スクリーンショット 2023-10-16 084438.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/865a8e1a-10f9-78de-c2a9-6bd504d4d96d.png)

<details><summary>:page_facing_up: コード</summary><div>

```python
import tkinter as tk
from tkinter import filedialog, ttk
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import customtkinter as ctk

def select_file_path(entry):
    """ファイル選択ダイアログを開き、選択されたファイルのパスをエントリボックスに設定する"""
    filepath = filedialog.askopenfilename(title="CSVファイルを選択してください", filetypes=[("CSV files", "*.csv")])
    entry.delete(0, tk.END)  # エントリボックスの内容をクリア
    entry.insert(0, filepath)  # 新しいパスをエントリボックスに設定

def plot_graph(entry, combobox, scale, canvas, frame_right):
    """CSVデータを読み込み、指定された線のスタイルでグラフを描画する"""
    filepath = entry.get()
    df = pd.read_csv(filepath)
    
    # 線のスタイルと太さを取得
    line_style = combobox.get()
    line_width = scale.get()

    # 既存のキャンバスオブジェクトの図をクリア
    plt.close('all')
    
    # フレームのサイズからFigureのサイズを計算
    fig_width = frame_right.winfo_width() / canvas.figure.dpi
    fig_height = (frame_right.winfo_height() - 60) / canvas.figure.dpi  # 50を引くのは"Save image"ボタンの分

    # プロット
    fig, ax = plt.subplots(figsize=(fig_width, fig_height))
    for column in df.columns[1:]:
        df.plot(x=df.columns[0], y=column, kind="line", ax=ax, linestyle=line_style, linewidth=line_width)

    # 既存のキャンバスオブジェクトに新しい図を描画
    canvas.figure = fig
    canvas.draw()
    
def save_image(canvas):
    """キャンバス上のグラフを画像として保存する"""
    canvas.figure.savefig("saved_graph.png")

def main():
    ctk.set_appearance_mode("dark")
    # ウィンドウ作成
    root = ctk.CTk()
    root.title("CSV Graph Plotter")
    
    # フレームの作成と配置
    frame_top = ctk.CTkFrame(root)
    frame_left = ctk.CTkFrame(root)
    frame_right = ctk.CTkFrame(root)

    frame_top.pack(fill=ctk.X, side=ctk.TOP, padx=5, pady=5)
    frame_left.pack(fill=ctk.Y, side=ctk.LEFT, padx=5, pady=5)
    frame_right.pack(fill=ctk.BOTH, expand=True, side=ctk.RIGHT, padx=5, pady=5)

    entry_filepath = ctk.CTkEntry(frame_top, width=300)
    entry_filepath.pack(fill=ctk.X, side=ctk.LEFT, padx=5, pady=5)  # fill=ctk.X を追加
    btn_select_file = ctk.CTkButton(frame_top, text="Select file", command=lambda: select_file_path(entry_filepath))
    btn_select_file.pack(side=ctk.LEFT, padx=5, pady=5)
    btn_open_file = ctk.CTkButton(frame_top, text="Open file", command=lambda: plot_graph(entry_filepath, combobox_linetype, scale_linewidth, canvas, frame_right))
    btn_open_file.pack(side=ctk.LEFT, padx=5, pady=5)

    # 左側フレーム (ピンク色のフレーム)
    label_linetype = ctk.CTkLabel(frame_left, text="Linetype")
    label_linetype.pack(pady=5)
    combobox_linetype = ctk.CTkComboBox(frame_left, values=["-", "--", "-.", ":"])
    combobox_linetype.pack(pady=5)
    combobox_linetype.set("-")

    label_linewidth = ctk.CTkLabel(frame_left, text="Line width")
    label_linewidth.pack(pady=5)
    scale_linewidth = ctk.CTkSlider(frame_left, from_=1, to=5)
    scale_linewidth.set(2)
    scale_linewidth.pack(pady=5)

    # 右側フレーム (緑色のフレーム)
    btn_save_image = ctk.CTkButton(frame_right, text="Save image", command=lambda: save_image(canvas))
    btn_save_image.pack(pady=10, anchor='s')
    canvas = FigureCanvasTkAgg(plt.figure(), master=frame_right)
    canvas.get_tk_widget().pack(fill=ctk.BOTH, expand=True)

    root.mainloop()

if __name__ == "__main__":
    main()
```

* CTkComboBox と CTkSliderは今のGPTは知らないために使ってくれなかったので、追加で教えて使用してもらうようにしました。

</div></details>

実行結果は以下の通りです。
![app5-2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/3b2b1a8b-6dc6-dc0d-154d-ed285ff2d970.gif)
完成しました！

# 5. おわりに

　デザインの細かいところは自分で修正する方が早かったり、拡張性の高いコードにするためには細かい指示が必要だったり、100点とは言えないですが、どこから作ればいいかわからないという人にはすごく便利になったと思います。

ただ今回紹介した GPT-4V のすごいところは、試してみた幾何学的な図形の読み込みというよりも、一般的な写真やイラストの認識精度だと思います。
（すでにWebでたくさんの例が上がっているので省略しますが、どんな画像を渡しても、自分より語彙力が豊富でわかりやすい説明が返ってきます...）

猫の識別ができたとニュースになってAIブームが始まったのが2012年ごろで，その約10年でこんなことになってしまうとは...