😮

[Python]Google AI Studio + Code Execution + Gemini 2.5 Proで遊んでみた(PIL)

に公開

TL;DR

  • Google AI StudioのCode Executionで生成したPythonのコードを実行できる
  • あいまいなプロンプトでもGemini 2.5 Proの強力な推論でコードを大量に生成できる
  • MatplotlibのShowを実行するとグラフを表示できるが、Pillow(PIL)と組み合わせることで画像も表示できる
  • PythonはNumPyでベクトル演算や行列演算を簡単に行うことができるため、Pillowで取得した画像データを簡単に編集、解析できる
  • 結果、PhotoshopやGIMP、Pixlrのような画像編集ソフトウェアを操作しなくてもAIが画像を解析して自動的に編集してくれる
  • メリット・デメリットそれぞれあるが、遊べるうちに遊んでおきたい

はじめに

https://ai.dev/

無料でAPIキーを取得する際初めて使用したGoogle AI Studioですが、Gemini 2.5 Proがいち早く使えるということで数日触ってみたところ、Gemini APIで使える様々なツールもGUI操作で簡単に使えることが分かり、特にPythonのコードを実行できるCode Executionで様々なことができることが分かったので、数日間遊んでみた感想を書いていきたいと思います。

Gemini 2.5 Pro

まずGemini 2.5 ProはGemini 2.0 Flashと何が違うのか以下の一覧で確認してみましょう。

特徴 gemini-2.5-pro-exp-03-25 gemini-2.0-flash-thinking-exp-01-21 gemini-2.0-flash gemini-2.0-flash-lite
モデルのデプロイ状況 試験運用版 試験運用版 安定版 安定版
入力対応データタイプ テキスト、画像、動画、音声 テキスト、画像 テキスト、画像、動画、音声 テキスト、画像、動画、音声
出力対応データタイプ テキスト テキスト テキスト、画像 (試験運用版)、音声 (近日提供予定) テキスト
入力トークン数 1,048,576 1,048,576 1,048,576 1,048,576
出力トークン数 65,536 65,536 8,192 8,192
知識のカットオフ 2025年1月 2024年6月 2024年6月 2024年6月
構造化出力
関数呼び出し
ツールとしての検索
コードの実行
思考モデル

参考にした情報は以下のとおりです。

https://ai.google.dev/gemini-api/docs/models?hl=ja
https://deepmind.google/technologies/gemini/

  • 安定版: 特定の安定版モデルを参照します。通常、安定したモデルは変更されません。ほとんどの本番環境アプリでは、特定の安定したモデルを使用する必要があります。
  • 試験運用版: 試験運用版モデルを指します(本番環境での使用は想定していません)。Google は、フィードバックの収集、最新のアップデートをデベロッパーに迅速に提供すること、Google のイノベーションのペースを強調することを目的として、試験運用版モデルをリリースしています。

Google AI Studioを通じてGemini 2.5 Proを試す場合、無料枠の範囲内であれば無料で利用できますが、以下の点に注意が必要です。

  • レート制限: 無料枠には1分あたりのリクエスト数に制限があります。この制限を超えて利用したい場合は、従量課金制のプランにアップグレードする必要があります。
  • 対象モデル: 今後登場する新しいモデルや、より高性能なモデルは、無料枠の対象外となるか、異なる条件が設定される可能性があります。
  • 将来の変更: 無料枠の提供条件(レート制限、対象モデルなど)は、将来変更される可能性があります。Google Cloudの無料枠と同様に、予告なく変更される場合があるため、常に最新情報を確認することをお勧めします。
  • 本格的な商用利用: 大量のトラフィックが見込まれる本格的な商用アプリケーションでは、無料枠のレート制限では不十分な場合が多く、従量課金プランの利用が現実的です。

https://ai.google.dev/gemini-api/docs/pricing?hl=ja

なお、昨日2025/03/30よりgemini.google.comのような一般消費者向けGemini AppでもGemini 2.5 Proを使用できるようになりました。

https://x.com/GeminiApp/status/1906131622736679332

Google AI Stuido

では開発者向けのGoogle AI Stuidoでは何ができるのか確認していきましょう。

gemini

英語がたくさんあってよく分からないので、早速Gemini 2.5 Proの出番です。

You:
このGoogle AI Studio画面の機能について解説して

Gemini 2.5 Pro:
この画面は **Google AI Studio** のインターフェースです。Googleの生成AIモデル(この場合はGemini)を試したり、APIを使ったアプリケーション開発のプロトタイピングを行ったりするためのウェブベースのツールです。

各部分の機能を解説します。

**1. 左サイドバー (ナビゲーションと主要機能)**

*   **Google AI Studio**: ツールのタイトルです。
*   **Get API key**: Gemini APIを利用するためのAPIキーを取得する場所へのリンクです。プログラムからAIモデルを使う際に必要になります。
*   **Create Prompt**: 新しいプロンプト(AIへの指示)を作成・編集するメインの作業画面を開きます。(現在表示されている画面がこれにあたります)
*   **Stream Realtime**: リアルタイムで応答をストリーミング表示する機能かもしれません。
*   **Starter Apps**: すぐに試せるサンプルアプリケーションやテンプレート集です。
*   **Tune a Model**: 特定のタスクに合わせてモデルのファインチューニング(微調整)を行う機能です。
*   **Library**: 作成したプロンプトやチューニングしたモデルなどを保存・管理する場所です。
*   **Enable chat history**: 会話履歴の保存を有効/無効にする設定です。
*   **Prompt Gallery**: Googleやコミュニティが提供する様々なプロンプトの例を見ることができるギャラリーです。
*   **API documentation**: Gemini APIの公式ドキュメントへのリンクです。
*   **Developer forum**: 開発者向けのフォーラム(質問や情報交換の場)へのリンクです。
*   **Changelog**: Google AI Studioやモデルの変更履歴・アップデート情報です。
*   **This experimental model...**: 現在選択されているモデルが実験的なものであり、フィードバックやテスト目的であり、本番環境での使用は推奨されない旨の注意書きです。
*   **Settings**: Google AI Studio自体の設定(テーマなど)を行う場所です。

**2. 中央メインエリア (プロンプト作成・実行)**

*   **System Instructions**: (折りたたまれていますが) AIモデルに対して、ユーザープロンプトの前に与える全体的な指示や前提条件を設定できます。キャラクター設定や回答スタイルの指定などに使います。
*   **What will you build?**: ユーザーに開発を促す見出しです。
*   **Push Gemini to the limits...**: Gemini APIを使ってAIの可能性を探ることを奨励するサブテキストです。
*   **3つのボックス (Tone Changer, Santa's Mailbox, Video Q&A)**: クリックするとすぐに試せるプロンプトの例(スタータープロンプト)です。
    *   `Tone Changer`: 文章のトーン(口調)を調整する例。
    *   `Santa's Mailbox`: サンタクロースとして手紙に返信する例。
    *   `Video Q&A`: 動画の内容について質問応答する例。
*   **(下部の入力欄) Type something**: ここにAIへの指示(プロンプト)を入力します。
*   **(+) アイコン**: テキストだけでなく、画像などの他の種類のデータをプロンプトに追加する機能と思われます。
*   **Run (Ctrl+<-)**: 入力したプロンプトを実行し、AIからの応答を生成させるボタンです。
*   **ゴミ箱アイコン**: 入力内容や応答をクリアする機能と思われます。

**3. 右サイドバー (実行設定とツール)**

*   **Run settings**: AIを実行する際の詳細な設定項目です。
*   **Get code**: 現在のプロンプトと設定を、Python、cURLなどのコードとしてエクスポートする機能です。API連携する際に便利です。
*   **Model**: 使用するAIモデルを選択します。現在は `Gemini 1.5 Pro Experimental 03-25` が選択されています。
*   **Token count**: 現在のプロンプトと応答のトークン数(AIが処理するテキストの単位)と、モデルの上限トークン数を表示します。
*   **Temperature**: AIの応答のランダム性(創造性)を調整するパラメータです。値が低いほど決まった応答をしやすく、高いほど多様で創造的な応答をしやすくなります。現在は `1` に設定されています。
*   **Tools**: AIの能力を拡張するためのツール設定です。
    *   `Structured output`: AIの出力を特定の形式(JSONなど)に強制する機能です。(現在はオフ)
    *   `Code execution`: AIがコード(Pythonなど)を実行できるようにする機能です。(現在はオフ)
    *   `Function calling`: AIが外部のAPIや事前に定義された関数を呼び出せるようにする機能です。(現在はオフ、Editで設定可能)
    *   `Grounding with Google Search`: AIがGoogle検索を利用して、より最新の情報や事実に基づいた応答を生成できるようにする機能です。(現在はオフ)
*   **Advanced settings**: 高度な設定項目です。
    *   `Safety settings`: 有害なコンテンツ(ヘイトスピーチ、危険な内容など)のフィルタリング強度を設定します。`Edit safety settings` で詳細設定が可能です。
    *   `Add stop sequence`: AIが特定の文字列を生成したら、それ以上生成を続けないように停止シーケンスを設定します。

**4. 上部バー**

*   **Enable Autosave**: 作業内容の自動保存を有効/無効にします。
*   **Compare**: 異なるモデルや設定での応答結果を比較する機能かもしれません。
*   **... (More options)**: その他のメニュー項目(ファイル操作など)が含まれている可能性があります。
*   **Ta (アカウント情報)**: 現在ログインしているGoogleアカウントを示します。

**まとめ**

Google AI Studioは、Geminiモデルを使って以下のようなことを行うための統合開発環境(のようなもの)です。

*   様々なプロンプトを試してAIの能力を理解する。
*   TemperatureやSafety Settingsなどのパラメータを調整して応答を制御する。
*   Function CallingやGoogle Search連携などのツールを活用する。
*   APIを利用したアプリケーション開発のためのプロトタイピングやコード生成を行う。
*   モデルのファインチューニングを行う(高度な機能)。

特に、プロンプトエンジニアリングの実験や、APIを使った開発を始める前のテストに非常に便利なツールです。

今回は以下の機能だけ使いますので、オフになっている場合はオンにしましょう。

    *   `Code execution`: AIがコード(Pythonなど)を実行できるようにする機能です。(現在はオフ)

また、入力欄周辺も確認しておきましょう。

*   **(下部の入力欄) Type something**: ここにAIへの指示(プロンプト)を入力します。
*   **(+) アイコン**: テキストだけでなく、画像などの他の種類のデータをプロンプトに追加する機能と思われます。
*   **Run (Ctrl+<-)**: 入力したプロンプトを実行し、AIからの応答を生成させるボタンです。
*   **ゴミ箱アイコン**: 入力内容や応答をクリアする機能と思われます。

画像をアップロードする時は(+)アイコンをクリックして、"Upload Image"を選択します。
その他の選択項目は今回使いません。

Code Execution

試しに以下のプロンプトを入力してみます。

You:
Pythonで7 * 7 * 73 * 127 * 337 * 92737 * 649657を計算して

すると以下のような結果が表示されます。

Gemini 2.5 Pro:
はい、承知いたしました。Pythonを使用して 7 * 7 * 73 * 127 * 337 * 92737 * 649657 を計算します。
Executable code
result = 7 * 7 * 73 * 127 * 337 * 92737 * 649657
print(f'{result=}')
Code execution result
result=9223372036854775807
Gemini 2.5 Pro:
計算結果は 9,223,372,036,854,775,807 です。

ちゃんと計算されていることが分かりました。

なお、Code Executionを使わないとGemini 2.5 Proであっても以下のように誤った答えを出力します。

You:
7 * 7 * 73 * 127 * 337 * 92737 * 649657を計算して

Gemini 2.5 Pro:
はい、承知いたしました。計算結果は以下の通りです。

7 * 7 * 73 * 127 * 337 * 92737 * 649657 = **9,224,053,118,531,987,027**

読み方は「922京4053兆1185億3198万7027」となります。

ちなみにこのCode ExecutionはGRTE(Google Runtime Environment)内のgVisorサンドボックスで動いているそうです。

https://www.landh.tech/blog/20250327-we-hacked-gemini-source-code
https://news.ycombinator.com/item?id=43508418

Python

さて、このCode Executionでは何ができて、何ができないのでしょうか?
ドキュメントを確認してみましょう。

https://ai.google.dev/gemini-api/docs/code-execution?hl=ja&lang=python

Gemini API のコード実行機能を使用すると、モデルは Python コードを生成して実行し、最終的な出力に到達するまで結果から反復的に学習できます。このコード実行機能を使用すると、コードベースの推論を活用し、テキスト出力を生成するアプリケーションを構築できます。たとえば、方程式を解くアプリやテキストを処理するアプリでコード実行を使用できます。
コード実行は、AI Studio と Gemini API の両方で使用できます。AI Studio では、右側のパネルの [ツール] でコード実行を有効にできます。Gemini API は、関数呼び出しと同様に、コード実行をツールとして提供します。コード実行をツールとして追加すると、モデルがコード実行を使用するタイミングを決定します。
コード実行環境には、altair、chess、cv2、matplotlib、mpmath、numpy、pandas、pdfminer、reportlab、seaborn、sklearn、statsmodels、striprtf、sympy、tabulate のライブラリが含まれています。独自のライブラリをインストールすることはできません。

なるほど。
いくつか試してみましたが、以下のようなことはできないことが分かりました。

  • pythonコマンド(に似た何か)以外のコマンド呼び出し
  • DuckDBなど、コード実行環境に含まれていないライブラリの呼び出し
  • pipを使用した外部ライブラリのインストール
  • Pythonの標準ライブラリurllib.requestのHTTPクライアントでリクエストの送信

ただ本当に上記ライブラリしか使えないのでしょうか?

Pillow

試しに以下のようなプロンプトを入力してみます。

You:
PythonのPillowで丸を表示するコードを実行。コメント、他の出力は不要
Executable code
from PIL import Image, ImageDraw

img = Image.new('RGB', (200, 200), 'white')
draw = ImageDraw.Draw(img)
draw.ellipse((50, 50, 150, 150), fill='red')
# img.show() # This would open the image in a viewer locally.
# To show the image here, we save it and display it.
img.save("circle_image.png")

# Display the saved image path for confirmation (optional, but useful for context)
print("Image saved as circle_image.png")
Code execution result
Image saved as circle_image.png

circle_image

あれ???
Pillowは使用できるようです。

ひょっとすると、Matplotlibの依存関係だから使用できるのでしょうか?

https://matplotlib.org/stable/install/dependencies.html

いくつか試してみましたが、以下のライブラリは使用できることが分かりました。

  • Python 3.11.8の標準ライブラリ(SQLiteなど)
  • コード実行環境に含まれているライブラリ
  • 上記ライブラリの依存関係にあたるライブラリ

依存関係にあたるライブラリの、さらに依存関係にあたるライブラリも使えますので結構な数になりますが、実用性のあるライブラリをいくつかピックアップして、解説とあわせてCode Executionしてもらおうと思います。

You:
cryptography, jinja2, jsonschema, pyparsing, pillow, scipyの説明を行い、コード例を出力し実行

Gemini 2.5 Pro:
はい、承知いたしました。いくつかのPythonライブラリについて、それぞれの説明と簡単なコード例、およびその実行結果を示します。

1. cryptography

説明:
cryptographyは、Pythonで暗号化アルゴリズムを使用するための高レベルなインターフェースと低レベルなプリミティブを提供するライブラリです。対称暗号(例: AES)、非対称暗号(例: RSA)、ハッシュ関数(例: SHA-256)、鍵導出関数などが含まれており、安全なアプリケーションを構築するために不可欠です。特にFernetは、対称認証暗号化を簡単に行うための高レベルなレシピを提供します。

More

コード例と実行結果:
Fernetを使用してメッセージを暗号化し、復号化する例です。

Executable code
# cryptography ライブラリのインポート
from cryptography.fernet import Fernet

# 1. 鍵を生成
key = Fernet.generate_key()
print(f"生成された鍵: {key.decode()}") # デコードして表示

# 2. Fernet インスタンスを作成
f = Fernet(key)

# 3. 暗号化したいメッセージ (バイト列である必要あり)
message = b"これは秘密のメッセージです。"
print(f"元のメッセージ: {message.decode()}")

# 4. メッセージを暗号化
encrypted_message = f.encrypt(message)
print(f"暗号化されたメッセージ: {encrypted_message}")

# 5. メッセージを復号化
decrypted_message = f.decrypt(encrypted_message)
print(f"復号化されたメッセージ: {decrypted_message.decode()}")

# 6. 別の鍵で復号しようとするとエラーになることの確認 (コメントアウト)
# wrong_key = Fernet.generate_key()
# try:
#     Fernet(wrong_key).decrypt(encrypted_message)
# except Exception as e:
#     print(f"\n異なる鍵での復号試行時のエラー: {e}")
Code execution result
Code execution failed.
Traceback (most recent call last):
  File "<string>", line 12
    message = b"これは秘密のメッセージです。"
              ^^^^^^^^^^^^^^^^^
SyntaxError: bytes can only contain ASCII literal characters
Gemini 2.5 Pro:
すみません、コード例に日本語のバイト列を直接記述したためエラーが発生しました。バイト列に変換する際にエンコーディングを指定します。修正して再度実行します。
Executable code
# cryptography ライブラリのインポート
from cryptography.fernet import Fernet
import traceback

# 1. 鍵を生成
key = Fernet.generate_key()
print(f"生成された鍵: {key.decode()}") # デコードして表示

# 2. Fernet インスタンスを作成
f = Fernet(key)

# 3. 暗号化したいメッセージ
message_str = "これは秘密のメッセージです。"
message_bytes = message_str.encode('utf-8') # UTF-8でバイト列にエンコード
print(f"元のメッセージ: {message_str}")

# 4. メッセージを暗号化
encrypted_message = f.encrypt(message_bytes)
print(f"暗号化されたメッセージ: {encrypted_message}")

# 5. メッセージを復号化
decrypted_message_bytes = f.decrypt(encrypted_message)
decrypted_message_str = decrypted_message_bytes.decode('utf-8') # UTF-8で文字列にデコード
print(f"復号化されたメッセージ: {decrypted_message_str}")

# 6. 別の鍵で復号しようとするとエラーになることの確認
wrong_key = Fernet.generate_key()
try:
    Fernet(wrong_key).decrypt(encrypted_message)
except Exception as e:
    print(f"\n異なる鍵での復号試行時のエラータイプ: {type(e).__name__}") # エラータイプのみ表示
    # print(f"エラー詳細: {e}") # 詳細なエラーメッセージも表示可能
    # traceback.print_exc() # トレースバック全体を表示することも可能
Code execution result
生成された鍵: pT5BN4lpFEuXjQoMQ-h99HIl3xVSbPeylZJJmS0yEbY=
元のメッセージ: これは秘密のメッセージです。
暗号化されたメッセージ: b'gAAAAABn6WbQL47s83IdjsVGhH302M3yJmJqvQWl8CZrZGu50jZNqRtu8P4zLO5QKG_9vBXfd5xzxK_FGlvucgI_pukOFGu1FUkvsYGnBmYrHFoagoXUGaBBbhU2UyLxPBtZN7P5Jd2_'
復号化されたメッセージ: これは秘密のメッセージです。

異なる鍵での復号試行時のエラータイプ: InvalidToken

解説:
Fernetを使用して、鍵の生成、メッセージの暗号化、そして同じ鍵を用いた復号化を行いました。異なる鍵で復号しようとすると InvalidToken エラーが発生し、データが改ざんされたり、鍵が間違っている場合に復号できないことが確認できます。

2. jinja2

説明:
jinja2は、高速で表現力豊か、かつ拡張可能なPython用のテンプレートエンジンです。主にWebフレームワーク(Flask、Djangoなど)でHTMLを生成するために使われますが、設定ファイル、ソースコード、その他あらゆるテキストファイルの生成にも利用できます。変数置換、制御構造(if文、forループ)、テンプレート継承、マクロなどの機能を持っています。

More

コード例と実行結果:
簡単なテンプレート文字列と辞書データを使ってテキストを生成する例です。

Executable code
# jinja2 ライブラリのインポート
from jinja2 import Template

# 1. テンプレート文字列を定義
template_string = "こんにちは、{{ name }}さん! あなたの好きな果物は {{ favorite_fruit }} ですね。"

# 2. テンプレートオブジェクトを作成
template = Template(template_string)

# 3. テンプレートに渡すデータ(辞書)
data = {
    "name": "太郎",
    "favorite_fruit": "りんご"
}

# 4. テンプレートをレンダリング(データを使ってテキストを生成)
output_string = template.render(data)

print("テンプレート:")
print(template_string)
print("\nデータ:")
print(data)
print("\nレンダリング結果:")
print(output_string)
Code execution result
テンプレート:
こんにちは、{{ name }}さん! あなたの好きな果物は {{ favorite_fruit }} ですね。

データ:
{'name': '太郎', 'favorite_fruit': 'りんご'}

レンダリング結果:
こんにちは、太郎さん! あなたの好きな果物は りんご ですね。

解説:
{{ variable_name }} の形式で記述されたプレースホルダーが、renderメソッドに渡された辞書の対応する値で置き換えられています。

3. jsonschema

説明:
jsonschemaは、JSONデータが特定の構造(スキーマ)に準拠しているかを検証するためのライブラリです。APIリクエスト/レスポンスの検証、設定ファイルのフォーマットチェック、データの一貫性維持などに役立ちます。JSON Schema 仕様に基づいてスキーマを定義し、それに対してJSONデータを検証します。

More

コード例と実行結果:
ユーザー情報を表すJSONデータが、定義されたスキーマに合致するかを検証する例です。

Executable code
# jsonschema ライブラリのインポート
from jsonschema import validate
from jsonschema.exceptions import ValidationError

# 1. スキーマを定義 (どのような構造であるべきか)
schema = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "age": {"type": "integer", "minimum": 0},
        "email": {"type": "string", "format": "email"}
    },
    "required": ["name", "age"] # name と age は必須
}

# 2. 検証するJSONデータ (辞書形式)
# 2.1. 有効なデータ
valid_data = {
    "name": "山田 花子",
    "age": 30,
    "email": "hanako.yamada@example.com"
}

# 2.2. 無効なデータ (ageが数値でない)
invalid_data_age = {
    "name": "鈴木 一郎",
    "age": "二十歳", # 数値ではない
    "email": "ichiro.suzuki@example.com"
}

# 2.3. 無効なデータ (必須項目 email がない、ただし email は required ではない)
# email は必須ではないので、これはスキーマ上は有効
valid_data_no_email = {
    "name": "佐藤 次郎",
    "age": 25
}

# 2.4. 無効なデータ (必須項目 name がない)
invalid_data_no_name = {
    "age": 40,
    "email": "jiro.sato@example.com"
}


print("スキーマ:")
print(schema)

# 3. 検証の実行と結果表示
print("\n--- 検証結果 ---")

# 3.1. 有効なデータの検証
try:
    validate(instance=valid_data, schema=schema)
    print(f"\nデータ1 {valid_data}: 有効です。")
except ValidationError as e:
    print(f"\nデータ1 {valid_data}: 無効です。エラー: {e.message}")

# 3.2. emailがないデータの検証 (emailは必須ではないので有効)
try:
    validate(instance=valid_data_no_email, schema=schema)
    print(f"\nデータ2 {valid_data_no_email}: 有効です。 (emailは必須ではない)")
except ValidationError as e:
    print(f"\nデータ2 {valid_data_no_email}: 無効です。エラー: {e.message}")

# 3.3. age が不正なデータの検証
try:
    validate(instance=invalid_data_age, schema=schema)
    print(f"\nデータ3 {invalid_data_age}: 有効です。")
except ValidationError as e:
    print(f"\nデータ3 {invalid_data_age}: 無効です。エラー: {e.message}")

# 3.4. name がないデータの検証
try:
    validate(instance=invalid_data_no_name, schema=schema)
    print(f"\nデータ4 {invalid_data_no_name}: 有効です。")
except ValidationError as e:
    print(f"\nデータ4 {invalid_data_no_name}: 無効です。エラー: {e.message}")
Code execution result
スキーマ:
{'type': 'object', 'properties': {'name': {'type': 'string'}, 'age': {'type': 'integer', 'minimum': 0}, 'email': {'type': 'string', 'format': 'email'}}, 'required': ['name', 'age']}

--- 検証結果 ---

データ1 {'name': '山田 花子', 'age': 30, 'email': 'hanako.yamada@example.com'}: 有効です。

データ2 {'name': '佐藤 次郎', 'age': 25}: 有効です。 (emailは必須ではない)

データ3 {'name': '鈴木 一郎', 'age': '二十歳', 'email': 'ichiro.suzuki@example.com'}: 無効です。エラー: '二十歳' is not of type 'integer'

データ4 {'age': 40, 'email': 'jiro.sato@example.com'}: 無効です。エラー: 'name' is a required property

4. pyparsing

説明:
pyparsingは、正規表現よりも複雑なテキスト構造や文法を定義し、解析するためのPythonライブラリです。再帰的な文法、特定の順序や組み合わせを持つ要素、無視するべき要素(例: コメント)などを柔軟に指定できます。BNF(バッカス・ナウア記法)のような形式で文法をPythonコードとして記述し、テキストを解析して構造化されたデータ(リストや辞書)に変換することができます。設定ファイル、ログファイル、簡単な独自言語などの解析に適しています。

More

コード例と実行結果:
カンマ区切りの数値リスト(例: "1, 2, 3, 4")を解析する簡単な例です。

Executable code
# pyparsing ライブラリのインポート
from pyparsing import delimitedList, Word, nums, ParseException

# 1. 基本要素の定義
# 数値: 1文字以上の数字 ('0'-'9') からなる単語
integer = Word(nums)
# 数値をPythonのint型に変換するアクションを追加
integer.setParseAction(lambda t: int(t[0]))

# 2. 文法の定義
# カンマ区切りの数値リスト (数値が1つ以上)
# delimitedListは、第一引数の要素が、第二引数(デフォルトはカンマ)で区切られたリストを解析
number_list = delimitedList(integer)

# 3. 解析する文字列
text_to_parse = "10, 25, 7, 105"
invalid_text = "10, twenty, 30" # 不正な形式

print(f"解析対象の文字列: '{text_to_parse}'")
print(f"不正な形式の文字列: '{invalid_text}'")

# 4. 解析の実行と結果表示
try:
    # parseString は解析結果を ParseResults オブジェクトで返す
    # asList() で Python のリストとして取得できる
    parsed_result = number_list.parseString(text_to_parse, parseAll=True)
    print(f"\n'{text_to_parse}' の解析結果: {parsed_result.asList()}")
    print(f"解析結果の型: {type(parsed_result)}")
    print(f"リストとしての型: {type(parsed_result.asList())}")
    print(f"リストの要素の型: {type(parsed_result.asList()[0])}")

except ParseException as e:
    print(f"\n'{text_to_parse}' の解析に失敗しました: {e}")

# 5. 不正な文字列の解析試行
try:
    parsed_invalid = number_list.parseString(invalid_text, parseAll=True)
    print(f"\n'{invalid_text}' の解析結果: {parsed_invalid.asList()}")
except ParseException as e:
    # parseAll=True なので、完全にマッチしないとエラーになる
    # エラーメッセージには、期待していたものと、問題が発生した位置が表示される
    print(f"\n'{invalid_text}' の解析に失敗しました (想定通り):")
    print(f"  エラーメッセージ: {e}")
    print(f"  エラー発生位置: {e.loc}")
    print(f"  問題の箇所: '{e.line}'") # エラーが発生した行全体
    # print(f"  詳細: {e.explain()}") # より詳細な説明
Code execution result
解析対象の文字列: '10, 25, 7, 105'
不正な形式の文字列: '10, twenty, 30'

'10, 25, 7, 105' の解析結果: [10, 25, 7, 105]
解析結果の型: <class 'pyparsing.version_2_2.ParseResults'>
リストとしての型: <class 'list'>
リストの要素の型: <class 'int'>

'10, twenty, 30' の解析に失敗しました (想定通り):
  エラーメッセージ: Expected end of text (at char 2), (line:1, col:3)
  エラー発生位置: 2
  問題の箇所: '10, twenty, 30'

解説:
Word(nums)で数字の連続を定義し、.setParseAction()でそれをPythonのint型に変換するように指定しました。delimitedList()でカンマ区切りのリスト文法を定義し、parseString()で文字列を解析しました。解析結果はParseResultsオブジェクトとして得られ、.asList()でPythonのリストに変換できます。不正な形式の文字列を解析しようとするとParseExceptionが発生し、どこで問題が起きたかの情報が得られます。
補足: ParseException: Expected end of text (at char 2), (line:1, col:3) というエラーメッセージは少し誤解を招きやすいかもしれません。parseAll=True を指定した場合、parseString は文字列全体が定義された文法にマッチすることを期待します。最初の 10integer として正しく解析されますが、次の ,delimitedList の区切り文字として処理されます。しかし、その後の twentyinteger として解析できないため、エラーとなります。エラーメッセージの Expected end of text は、10 を解析した後、もしそれがリストの最後の要素なら文字列の終わりが来るはず、あるいは区切り文字と次の integer が来るはず、という文脈で解釈すると理解しやすいかもしれません。エラー位置 (at char 2) は、10 の直後、カンマの前を示しています。

5. pillow

説明:
pillowは、Python Imaging Library (PIL) の後継であり、Pythonで画像ファイルを扱ったり、画像処理を行ったりするための強力なライブラリです。様々な画像フォーマット(JPEG, PNG, GIF, TIFF, BMPなど)の読み込み、保存、表示が可能です。また、リサイズ、回転、トリミング、色の変換(グレースケール化、RGBA変換など)、フィルター処理(ぼかし、シャープ化など)、ピクセル単位の操作、描画機能(テキスト、線、図形)などを提供します。Webアプリケーションでのサムネイル生成、画像解析の前処理、画像への注釈追加など、幅広い用途で使われます。

More

コード例と実行結果:
新しい画像をメモリ上に生成し、そのサイズとモード(色の形式)を表示する例です。

Executable code
# pillow ライブラリ (PILのフォーク) から Image モジュールをインポート
from PIL import Image

try:
    # 1. 新しい画像を生成
    # Image.new(mode, size, color)
    # mode: 'RGB' (カラー), 'L' (グレースケール), 'RGBA' (アルファチャンネル付きカラー)など
    # size: (幅, 高さ) のタプル
    # color: 色 (モードによって指定方法が異なる。'RGB'なら(R, G, B)タプルや色名文字列)
    img_width = 200
    img_height = 100
    img_color = "skyblue" # 色名で指定
    # img_color = (135, 206, 235) # RGBタプルでも指定可能

    img = Image.new('RGB', (img_width, img_height), color=img_color)

    # 2. 画像の情報を表示
    print(f"生成された画像のモード: {img.mode}")
    print(f"生成された画像のサイズ (幅x高さ): {img.size}")
    print(f"画像の幅: {img.width}")
    print(f"画像の高さ: {img.height}")
    print(f"画像のフォーマット: {img.format}") # ファイルから読み込んでいないのでNone

    # 3. 簡単な操作 (例: グレースケールに変換)
    gray_img = img.convert('L')
    print(f"\nグレースケール変換後のモード: {gray_img.mode}")
    print(f"グレースケール変換後のサイズ: {gray_img.size}") # サイズは変わらない

    # 4. 画像の保存 (ファイルに保存する場合 - コメントアウト)
    # output_filename = "generated_image.png"
    # try:
    #     img.save(output_filename)
    #     print(f"\n画像を '{output_filename}' として保存しました。")
    #     # 保存した画像の情報を確認
    #     saved_img = Image.open(output_filename)
    #     print(f"保存された画像のフォーマット: {saved_img.format}")
    #     saved_img.close() # ファイルを閉じる
    # except Exception as e:
    #     print(f"\n画像の保存中にエラーが発生しました: {e}")
    # finally:
    #      # テスト用に作成したファイルを削除 (もし存在すれば)
    #      import os
    #      if os.path.exists(output_filename):
    #          os.remove(output_filename)
    #          # print(f"'{output_filename}' を削除しました。")


except ImportError:
    print("Pillowがインストールされていません。'pip install Pillow'でインストールしてください。")
except Exception as e:
    print(f"エラーが発生しました: {e}")
Code execution result
生成された画像のモード: RGB
生成された画像のサイズ (幅x高さ): (200, 100)
画像の幅: 200
画像の高さ: 100
画像のフォーマット: None

グレースケール変換後のモード: L
グレースケール変換後のサイズ: (200, 100)

解説:
Image.new()を使って、指定したモード('RGB')、サイズ(幅200, 高さ100)、色('skyblue')で新しい画像オブジェクトをメモリ上に作成しました。作成した画像オブジェクトの属性(.mode, .size, .width, .height, .format)にアクセスして情報を表示しています。.formatはファイルから読み込んだ場合にそのフォーマット(例: 'PNG', 'JPEG')が入りますが、今回は新規作成なのでNoneになっています。.convert('L')メソッドで画像をグレースケールに変換し、そのモードとサイズも確認しました。画像処理を行っても、通常サイズは維持されます。
コード例の後半には、画像をファイルに保存する処理をコメントアウトして含めています(実行環境にファイル書き込み権限がない場合があるため)。

6. scipy

説明:
scipy(サイパイと読みます)は、科学技術計算のための基本的なアルゴリズムと便利な関数を集めたPythonライブラリです。NumPyを基盤としており、NumPyが提供する多次元配列(ndarray)を効率的に操作するための高度な機能を提供します。

scipyは多くのサブモジュールで構成されており、それぞれが特定の分野の計算を担当しています。主なサブモジュールには以下のようなものがあります:

  • scipy.optimize: 関数の最適化(最小値/最大値探索)、根の探索、曲線フィッティングなど。
  • scipy.integrate: 数値積分(定積分、常微分方程式の求解)。
  • scipy.linalg: 線形代数ルーチン。NumPyのlinalgよりも高度な機能(例えば、より多くの種類の行列分解)を含みます。
  • scipy.stats: 統計関数、確率分布、統計検定など。
  • scipy.interpolate: データの補間(点と点の間を滑らかにつなぐ)。
  • scipy.fft: 高速フーリエ変換 (FFT)。
  • scipy.signal: 信号処理ツール(フィルタリング、窓関数など)。
  • scipy.sparse: 疎行列(要素のほとんどがゼロである行列)とその関連アルゴリズム。
  • scipy.spatial: 空間データ構造とアルゴリズム(k-d木、距離計算、凸包など)。
  • scipy.io: ファイルの読み書き(MATLAB、NetCDF、WAVファイルなど)。

scipyは、データサイエンス、機械学習、物理シミュレーション、工学計算など、数値計算が必要とされる様々な分野で広く利用されています。

More

コード例と実行結果:
scipy.optimizeモジュールのminimize関数を使用して、簡単な二次関数の最小値を見つける例です。関数 f(x) = (x-3)^2 + 1 の最小値を求めます。この関数の最小値は x=3 のとき f(3)=1 となることが解析的にわかっています。

Executable code
# scipy.optimize モジュールから minimize 関数をインポート
from scipy.optimize import minimize
import numpy as np # NumPyもよく一緒に使われます

# 1. 最小化したい関数を定義
# f(x) = (x - 3)^2 + 1
def objective_function(x):
  return (x - 3)**2 + 1

# 2. 最適化の初期推定値 (どこから探索を開始するか)
initial_guess = 0.0
print(f"最小化対象の関数: f(x) = (x - 3)^2 + 1")
print(f"探索の初期値: x = {initial_guess}")

# 3. minimize 関数を実行
# minimize(関数, 初期値, method='BFGS' など)
# methodを指定しない場合、適したソルバーが自動選択されます (ここではデフォルト)
result = minimize(objective_function, initial_guess)

# 4. 結果を表示
print("\n--- 最適化結果 ---")
if result.success:
    print(f"最適化は成功しました。")
    # result.x は最小値を与える x の値 (配列で返る)
    optimal_x = result.x[0]
    # result.fun は関数 f(x) の最小値
    min_value = result.fun
    print(f"関数が最小となる x の値: {optimal_x:.6f}")
    print(f"そのときの関数の最小値 f(x): {min_value:.6f}")
    # print(f"メッセージ: {result.message}") # より詳細なメッセージ
    # print(f"評価回数: {result.nfev}") # 関数評価回数
else:
    print(f"最適化は失敗しました。")
    print(f"終了ステータス: {result.status}")
    print(f"メッセージ: {result.message}")

# 解析的な解との比較
analytical_x = 3.0
analytical_min = 1.0
print(f"\n(参考) 解析的な解: x = {analytical_x}, f(x) = {analytical_min}")

# 結果が解析解に近いか簡単なチェック
assert np.isclose(result.x[0], analytical_x), "xの値が期待値と異なります"
assert np.isclose(result.fun, analytical_min), "関数の最小値が期待値と異なります"
print("\n数値計算結果は解析的な解とほぼ一致しました。")
Code execution result
最小化対象の関数: f(x) = (x - 3)^2 + 1
探索の初期値: x = 0.0

--- 最適化結果 ---
最適化は成功しました。
関数が最小となる x の値: 3.000000
そのときの関数の最小値 f(x): 1.000000

(参考) 解析的な解: x = 3.0, f(x) = 1.0

数値計算結果は解析的な解とほぼ一致しました。

解説:
objective_functionとして最小化したい関数を定義し、探索を開始する初期値 initial_guess を0.0としました。minimize関数は、指定された関数と初期値をもとに、関数の値が最も小さくなる点を探します。

実行結果を見ると、result.successTrue となり、最適化が成功したことがわかります。最小値を与える x の値 (result.x) は約3.0、そのときの関数の値 (result.fun) は約1.0となり、解析的に求めた解 (x=3, f(x)=1) と非常によく一致していることが確認できました。

このようにscipyを使うことで、複雑な数学的計算(この例では最適化)を少ないコードで実行できます。

以上、6つのライブラリをGemini 2.5 Proに解説してもらいましたが、どうやらPillowは実行環境にファイル書き込み権限がない場合を想定することが多く、必ず画像を表示することが成功するわけではないようです。

そこで注目したいのがドキュメントに書かれている以下の記述です。

注: コード実行によるグラフ レンダリングでは、matplotlib のみがサポートされています。

グラフの表示ができるMatplotlibにPillowの画像データを渡せば、画像の表示の成功確率は上がるのではないのでしょうか?

Matplotlib

前置きが長くなりましたが、PillowとMatplotlibを使い、様々な方法で遊んでみたいのでいくつか紹介してみたいと思います。

lenna

今回画像の編集、解析に使うメイドのレナちゃんです。
いつも通りDiffusersライブラリを使って画像生成AIで生成しました。

image

このような感じでプロンプトを実行しましょう。

You:
Pythonのpilとpltを使用してイラストをグレースケールにするコードを実行

Gemini 2.5 Pro:
はい、承知いたしました。
提供された画像ファイル `input_file_0.jpeg` をPythonのPillow (PIL) とMatplotlibライブラリを使用してグレースケールに変換し、表示するコードを実行します。

まず、画像を読み込んでグレースケールに変換し、その結果を表示します。
Executable code
# Import necessary libraries
from PIL import Image
import matplotlib.pyplot as plt

# Define the input filename
image_filename = "input_file_0.jpeg"

# Load the image
try:
    img = Image.open(image_filename)

    # Convert the image to grayscale
    # 'L' mode stands for Luminance (grayscale)
    img_gray = img.convert('L')

    # Display the grayscale image using matplotlib
    plt.figure(figsize=(6, 6)) # Adjust figure size if needed
    plt.imshow(img_gray, cmap='gray')
    plt.title("Grayscale Image")
    plt.axis('off') # Hide axes
    plt.show()

    print(f"Successfully converted '{image_filename}' to grayscale and displayed it.")

except FileNotFoundError:
    print(f"Error: File not found at '{image_filename}'")
except Exception as e:
    print(f"An error occurred: {e}")
Code execution result
Successfully converted 'input_file_0.jpeg' to grayscale and displayed it.

grayscale

一発で成功しました。
ただちょっと修正したいので、以下のコマンドを試してみます。
すると……。

You:
plt.title("Grayscale Image")を削除してイラストのみ表示

Gemini 2.5 Pro:
はい、承知いたしました。
Matplotlibで表示する際にタイトルを削除します。
Executable code
# Import necessary libraries (already imported in the previous step)
from PIL import Image
import matplotlib.pyplot as plt

# Define the input filename
image_filename = "input_file_0.jpeg"

# Load the image
try:
    img = Image.open(image_filename)

    # Convert the image to grayscale
    img_gray = img.convert('L')

    # Display the grayscale image using matplotlib, without the title
    plt.figure(figsize=(6, 6)) # Adjust figure size if needed
    plt.imshow(img_gray, cmap='gray')
    # plt.title("Grayscale Image") # Title removed as requested
    plt.axis('off') # Hide axes
    plt.show()

    print(f"Successfully displayed the grayscale image without a title.")

except FileNotFoundError:
    print(f"Error: File not found at '{image_filename}'")
except Exception as e:
    print(f"An error occurred: {e}")
Code execution result
Error: File not found at 'input_file_0.jpeg'

画像が見つからないエラーが表示されました。
なのでプロンプトを追記するたびに画像をアップロードしなければならない点に注意しましょう。

あるいは最初に実行したプロンプトでEditアイコンをクリックし、プロンプトに条件を追記する方法もあります。

edit

1. スクリーントーン

グレースケールにするだけなら別にPythonを使わなくてもできることなので、もっと複雑な処理に挑戦してみましょう。

https://ja.wikipedia.org/wiki/配列ディザリング

配列ディザリング(Ordered Dithering)と呼ばれるプログラミングにうってつけの処理があるので試してみます。

You:
Pythonのpilとpltを使用してイラストをモノクロの配列ディザリングでスクリーントーンにするコードを実行

Gemini 2.5 Pro:
はい、承知いたしました。PythonのPIL(Pillow)とMatplotlibを使用して、指定された画像をモノクロの配列ディザリングでスクリーントーン風に加工するコードを実行します。

まず、画像を読み込んでグレースケールに変換し、配列ディザリング(ここではBayer行列を使用)を適用します。最後に、元のグレースケール画像とディザリング後の画像を並べて表示します。
Code
Executable code
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt

# 画像を読み込む
img_path = 'input_file_0.jpeg'
img = Image.open(img_path)

# グレースケールに変換
gray_img = img.convert('L')

# 画像データをNumPy配列に変換 (0-255)
gray_data = np.array(gray_img, dtype=np.float64)

# --- 配列ディザリング (Ordered Dithering) ---
# Bayer matrix (4x4)
bayer_matrix_4x4 = np.array([
    [ 0,  8,  2, 10],
    [12,  4, 14,  6],
    [ 3, 11,  1,  9],
    [15,  7, 13,  5]
])

# マトリックスサイズ
matrix_size = bayer_matrix_4x4.shape[0]

# しきい値マトリックスを正規化 (0-1 の範囲にするため、最大値+1 = 16 で割る)
# 比較対象のピクセル値も 0-255 の範囲なので、しきい値も 0-255 の範囲にスケーリングする
threshold_matrix = (bayer_matrix_4x4 / (matrix_size * matrix_size)) * 255

# ディザリング結果を格納する配列を初期化
dithered_data = np.zeros_like(gray_data)

# 画像の各ピクセルを処理
height, width = gray_data.shape
for y in range(height):
    for x in range(width):
        # 対応するBayerマトリックスのしきい値を取得
        threshold = threshold_matrix[y % matrix_size, x % matrix_size]
        # ピクセルの輝度値としきい値を比較
        if gray_data[y, x] > threshold:
            dithered_data[y, x] = 255  # 白
        else:
            dithered_data[y, x] = 0   # 黒

# ディザリング後のデータを画像に変換
dithered_img = Image.fromarray(dithered_data.astype(np.uint8))

# --- 結果の表示 ---
fig, axes = plt.subplots(1, 2, figsize=(12, 6))

# 元のグレースケール画像
axes[0].imshow(gray_img, cmap='gray')
axes[0].set_title('Original Grayscale')
axes[0].axis('off')

# ディザリング後の画像
axes[1].imshow(dithered_img, cmap='gray')
axes[1].set_title(f'Dithered (Bayer {matrix_size}x{matrix_size})')
axes[1].axis('off')

plt.tight_layout()
plt.show()

# 必要であればディザリング後の画像を保存
# dithered_img.save('dithered_output.png')
# print("ディザリング後の画像を 'dithered_output.png' として保存しました。")

dithered

NumPyを使用してシンプルに書いてくれました。
ふたつ並べて表示してくれるのはありがたいですね!

2. 吹き出し

漫画っぽくしてみたくなったので、次は吹き出しに挑戦してみました。
しかしこれがなかなか難しい……。

というのも「ピース!」と日本語を表示しようとすると英語フォントを参照し、豆腐になってしまうからです。

そこで日本語フォントがないか確認するPythonのコードを実行してみたところ、以下パスにフォントがあることが分かりました。

/usr/share/fonts/GoogleSans-Regular.ttf
/usr/share/fonts/NotoSansCJK-Regular.ttc

どうやらMatplotlibで使用するためにあるようです。
Noto Sans CJKの日本語フォントを使用するようにプロンプトを改善してみましょう。

You:
Pythonのpilとpltを使用してイラストに「ピース!」の文字で吹き出しを追加するコードを実行
* 文字のフォントは/usr/share/fonts/NotoSansCJK-Regular.ttcを使用
* 吹き出しは白い四角形に白い三角形を合成したデザイン

Gemini 2.5 Pro:
はい、承知いたしました。
PythonのPillow(PIL)とMatplotlibを使用して、ご指定の画像に「ピース!」というテキストの吹き出しを追加します。

吹き出しのデザインは白い四角形と三角形を組み合わせ、テキストには指定されたフォントを使用します。
Code
Executable code
from PIL import Image, ImageDraw, ImageFont
import matplotlib.pyplot as plt
import numpy as np

# 画像を開く
img_path = "input_file_0.jpeg"
img = Image.open(img_path).convert("RGBA")
draw = ImageDraw.Draw(img)

# フォントを設定
font_path = "/usr/share/fonts/NotoSansCJK-Regular.ttc"
font_size = 50
try:
    font = ImageFont.truetype(font_path, font_size)
except IOError:
    print(f"フォントファイルが見つかりません: {font_path}. デフォルトフォントを使用します。")
    # 代替フォントやシステムデフォルトを探す (より堅牢なエラーハンドリング)
    # ここではシンプルな代替としてロードを試みる
    try:
        font = ImageFont.truetype("arial.ttf", font_size) # Windows/macOSで一般的なフォント
    except IOError:
        try:
            font = ImageFont.truetype("DejaVuSans.ttf", font_size) # Linuxで一般的なフォント
        except IOError:
            print("代替フォントも見つかりません。PILのデフォルトフォントを使用します。")
            font = ImageFont.load_default() # PILのデフォルト

# テキストと描画設定
text = "ピース!"
text_color = (0, 0, 0, 255) # 黒色
balloon_color = (255, 255, 255, 220) # 半透明の白

# テキストの描画に必要なサイズを取得 (bboxを使う)
# textbboxは(left, top, right, bottom)のタプルを返す
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
text_offset_x = bbox[0]
text_offset_y = bbox[1]

# 吹き出しのパディング
padding = 20

# 吹き出しの四角形の座標を計算
# 画像の左上あたりに配置
rect_x0 = 50
rect_y0 = 50
rect_x1 = rect_x0 + text_width + 2 * padding
rect_y1 = rect_y0 + text_height + 2 * padding

# 吹き出しの三角形(しっぽ)の座標を計算
# 四角形の右下から、ピースサインの方向(右下方向)へ伸ばす
tip_x = rect_x1 + 30
tip_y = rect_y1 + 30
triangle_points = [
    (rect_x1 - padding, rect_y1), # 四角形の右下の少し内側
    (rect_x1, rect_y1 - padding), # 四角形の右下の少し内側
    (tip_x, tip_y)          # しっぽの先端
]

# 吹き出しを描画(一時的なレイヤーに描画してアルファブレンディング)
# 元画像と同じサイズの透明なレイヤーを作成
overlay = Image.new('RGBA', img.size, (255, 255, 255, 0))
draw_overlay = ImageDraw.Draw(overlay)

# 吹き出しの四角形を描画
draw_overlay.rectangle([rect_x0, rect_y0, rect_x1, rect_y1], fill=balloon_color, outline=None)

# 吹き出しの三角形を描画
draw_overlay.polygon(triangle_points, fill=balloon_color, outline=None)

# 元画像と吹き出しレイヤーを合成
img = Image.alpha_composite(img, overlay)

# 合成後の画像にテキストを描画 (Drawオブジェクトを再作成)
draw = ImageDraw.Draw(img)
text_x = rect_x0 + padding - text_offset_x # bboxの左オフセットを考慮
text_y = rect_y0 + padding - text_offset_y # bboxの上オフセットを考慮
draw.text((text_x, text_y), text, font=font, fill=text_color)

# 結果を表示
plt.figure(figsize=(8, 8))
plt.imshow(np.array(img))
plt.axis('off') # 軸を非表示
plt.title("吹き出し付き画像")
plt.show()

# 必要であれば画像を保存
# output_path = "output_image_with_balloon.png"
# img.save(output_path)
# print(f"画像を保存しました: {output_path}")

balloon

やりました!!
吹き出しの色を半透明にしていたり、ちゃんとオフセットを考慮していてすごい!

3. カラーバランス調整

最後にイラストをより良くするために、カラーバランス調整を行ってみたいと思います。

You:
Pythonのpilとpltを使用してイラストを美しくするためにカラーバランス調整をすべて行ったコードを実行
* HSV色空間のヒストグラムを表示しながら調整
* ガンマ補正をかけた後、黒を青くする

Gemini 2.5 Pro:
はい、承知いたしました。PythonのPillowライブラリとMatplotlibライブラリを使用して、アップロードされたイラスト画像 (`input_file_0.jpeg`) のカラーバランス調整を行います。

以下の手順で進めます。

1.  画像を読み込みます。
2.  画像をHSV色空間に変換します。
3.  調整前のHSV各チャンネル(Hue: 色相, Saturation: 彩度, Value: 明度)のヒストグラムを表示します。
4.  Valueチャンネルにガンマ補正を適用します。
5.  RGB色空間に戻し、黒に近い部分を特定して青みを加える処理を行います。
6.  調整後の画像をHSV色空間に変換し、再度ヒストグラムを表示して変化を確認します。
7.  調整前後の画像を並べて表示します。

まずは、画像を読み込み、調整前のHSVヒストグラムを表示してみましょう。
Code
Executable code
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
import matplotlib.colors as mcolors

# 画像ファイルのパス
image_path = "input_file_0.jpeg"

# --- 1. 画像の読み込み ---
try:
    img_pil = Image.open(image_path).convert('RGB')
    img_rgb = np.array(img_pil) / 255.0  # 0-1 の範囲に正規化
    print(f"画像 '{image_path}' を読み込みました。サイズ: {img_pil.size}, モード: {img_pil.mode}")
except FileNotFoundError:
    print(f"エラー: 画像ファイル '{image_path}' が見つかりません。")
    # エラーが発生した場合、以降の処理をスキップ
    img_rgb = None
except Exception as e:
    print(f"画像の読み込み中にエラーが発生しました: {e}")
    img_rgb = None

# --- HSVヒストグラム表示関数 ---
def plot_hsv_histograms(img_rgb_norm, title):
    """RGB画像 (0-1正規化済み) を受け取り、HSVヒストグラムを表示する関数"""
    if img_rgb_norm is None:
        print("画像データがありません。ヒストグラムを表示できません。")
        return

    # RGBからHSVに変換 (matplotlib.colorsを使用, H/S/Vは0-1の範囲)
    img_hsv = mcolors.rgb_to_hsv(img_rgb_norm)

    # 各チャンネルを分離
    h = img_hsv[..., 0].flatten()
    s = img_hsv[..., 1].flatten()
    v = img_hsv[..., 2].flatten()

    # ヒストグラム描画
    fig, axs = plt.subplots(1, 3, figsize=(18, 4))
    fig.suptitle(title, fontsize=16)

    axs[0].hist(h * 360, bins=180, color='red', alpha=0.7) # Hは0-360度に変換して表示
    axs[0].set_title('Hue (色相)')
    axs[0].set_xlim(0, 360)
    axs[0].set_xlabel('角度 (°)')
    axs[0].set_ylabel('ピクセル数')

    axs[1].hist(s, bins=100, color='green', alpha=0.7)
    axs[1].set_title('Saturation (彩度)')
    axs[1].set_xlim(0, 1)
    axs[1].set_xlabel('彩度')

    axs[2].hist(v, bins=100, color='blue', alpha=0.7)
    axs[2].set_title('Value (明度)')
    axs[2].set_xlim(0, 1)
    axs[2].set_xlabel('明度')

    plt.tight_layout(rect=[0, 0.03, 1, 0.95]) # タイトルとの重なりを調整
    plt.show()

# --- 3. 調整前のHSVヒストグラム表示 ---
if img_rgb is not None:
    plot_hsv_histograms(img_rgb, "調整前のHSVヒストグラム")

# 元の画像も表示しておく
if img_rgb is not None:
    plt.figure(figsize=(6, 6))
    plt.imshow(img_rgb)
    plt.title("元の画像")
    plt.axis('off')
    plt.show()
Code execution result
画像 'input_file_0.jpeg' を読み込みました。サイズ: (832, 1216), モード: RGB

before
before hist

Gemini 2.5 Pro:
元の画像の読み込みと調整前のHSVヒストグラムの表示が完了しました。

ヒストグラムを見ると、以下の特徴がわかります。

Hue: 色相は主に暖色系(赤〜黄色、0〜60度付近)と、一部に青系の色(髪のハイライトなど、200〜240度付近?)が見られます。

Saturation: 彩度は低い値(0に近い)に偏っており、全体的に淡い色合いまたは無彩色(白、黒、灰色)が多いことを示唆しています。特に背景の白と服の黒・白の影響が大きいと考えられます。

Value: 明度は高い値(1に近い)に非常に集中しており、明るいピクセル(特に背景の白)が多いことがわかります。低い明度(0に近い、黒)のピクセルも存在します。

次に、ガンマ補正と「黒を青くする」処理を行います。ガンマ値と、黒と判断する閾値、青みの強さは調整可能です。ここでは例として以下の値を使用します。

ガンマ値: 1.2 (少し明るくする)

黒判定の閾値 (RGB各値): 0.2 (0-1スケールで各チャンネルがこの値以下)

青みの追加量: 青チャンネルに0.1を加える(最大1.0でクリップ)

これらの処理を適用し、調整後の画像とHSVヒストグラムを表示します。
Code
Executable code
# --- 4. ガンマ補正 ---
def apply_gamma_correction(img_rgb_norm, gamma=1.0):
    """RGB画像 (0-1正規化済み) にガンマ補正を適用する"""
    if img_rgb_norm is None:
        return None
    # ガンマ補正 (VチャンネルではなくRGB全体に適用する方が一般的で簡単)
    img_corrected = np.power(img_rgb_norm, 1.0 / gamma)
    # 値を0-1の範囲にクリップ
    img_corrected = np.clip(img_corrected, 0, 1)
    return img_corrected

# --- 5. 黒を青くする処理 ---
def make_blacks_blue(img_rgb_norm, threshold=0.2, blue_amount=0.1):
    """RGB画像 (0-1正規化済み) の黒い部分を青くする"""
    if img_rgb_norm is None:
        return None

    img_adjusted = img_rgb_norm.copy()
    # 黒に近いピクセルのマスクを作成
    # RGBすべての値が閾値以下の場合を黒と判定
    is_black_mask = np.all(img_adjusted <= threshold, axis=-1)

    # マスクされたピクセルの青チャンネル値を増やす
    img_adjusted[is_black_mask, 2] = np.clip(img_adjusted[is_black_mask, 2] + blue_amount, 0, 1)

    # オプション:青以外のチャンネルを少し減らすとより青みが強調される場合がある
    # img_adjusted[is_black_mask, 0] = np.clip(img_adjusted[is_black_mask, 0] - blue_amount * 0.1, 0, 1)
    # img_adjusted[is_black_mask, 1] = np.clip(img_adjusted[is_black_mask, 1] - blue_amount * 0.1, 0, 1)

    return img_adjusted

# --- 処理の実行 ---
gamma_value = 1.2
black_threshold = 0.2 # 0-1 scale
blue_addition = 0.1   # 0-1 scale

img_gamma_corrected = None
img_final = None

if img_rgb is not None:
    # ガンマ補正を適用
    img_gamma_corrected = apply_gamma_correction(img_rgb, gamma=gamma_value)
    print(f"ガンマ補正 (gamma={gamma_value}) を適用しました。")

    # 黒を青くする処理を適用
    img_final = make_blacks_blue(img_gamma_corrected, threshold=black_threshold, blue_amount=blue_addition)
    print(f"黒を青くする処理 (threshold={black_threshold}, blue_amount={blue_addition}) を適用しました。")

# --- 6. 調整後のHSVヒストグラム表示 ---
if img_final is not None:
    plot_hsv_histograms(img_final, f"調整後のHSVヒストグラム (Gamma={gamma_value}, Blue={blue_addition})")

# --- 7. 結果画像の表示 ---
if img_rgb is not None and img_final is not None:
    fig, axs = plt.subplots(1, 2, figsize=(12, 6))
    axs[0].imshow(img_rgb)
    axs[0].set_title("元の画像")
    axs[0].axis('off')

    axs[1].imshow(img_final)
    axs[1].set_title(f"調整後の画像 (Gamma={gamma_value}, Blue={blue_addition})")
    axs[1].axis('off')

    plt.tight_layout()
    plt.show()

    # 調整後の画像をファイルに保存(オプション)
    # final_image_pil = Image.fromarray((img_final * 255).astype(np.uint8))
    # final_image_pil.save("output_image.png")
    # print("調整後の画像を 'output_image.png' として保存しました。")
Code execution result
Code execution failed.
Traceback (most recent call last):
  File "<string>", line 40, in <module>
NameError: name 'img_rgb' is not defined
Gemini 2.5 Pro:
申し訳ありません、前のコードブロックで定義した変数が利用できない環境でした。画像を再度読み込み、必要な関数を定義し直してから処理を実行します。
Code
Executable code
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
import matplotlib.colors as mcolors

# 画像ファイルのパス
image_path = "input_file_0.jpeg"

# --- HSVヒストグラム表示関数 ---
def plot_hsv_histograms(img_rgb_norm, title):
    """RGB画像 (0-1正規化済み) を受け取り、HSVヒストグラムを表示する関数"""
    if img_rgb_norm is None:
        print("画像データがありません。ヒストグラムを表示できません。")
        return

    # RGBからHSVに変換 (matplotlib.colorsを使用, H/S/Vは0-1の範囲)
    # ゼロ除算を避けるために微小値を追加
    img_hsv = mcolors.rgb_to_hsv(np.clip(img_rgb_norm, 1e-6, 1.0))

    # 各チャンネルを分離
    h = img_hsv[..., 0].flatten()
    s = img_hsv[..., 1].flatten()
    v = img_hsv[..., 2].flatten()

    # ヒストグラム描画
    fig, axs = plt.subplots(1, 3, figsize=(18, 4))
    fig.suptitle(title, fontsize=16)

    axs[0].hist(h * 360, bins=180, color='red', alpha=0.7) # Hは0-360度に変換して表示
    axs[0].set_title('Hue (色相)')
    axs[0].set_xlim(0, 360)
    axs[0].set_xlabel('角度 (°)')
    axs[0].set_ylabel('ピクセル数')

    axs[1].hist(s, bins=100, color='green', alpha=0.7)
    axs[1].set_title('Saturation (彩度)')
    axs[1].set_xlim(0, 1)
    axs[1].set_xlabel('彩度')

    axs[2].hist(v, bins=100, color='blue', alpha=0.7)
    axs[2].set_title('Value (明度)')
    axs[2].set_xlim(0, 1)
    axs[2].set_xlabel('明度')

    plt.tight_layout(rect=[0, 0.03, 1, 0.95]) # タイトルとの重なりを調整
    plt.show()

# --- 4. ガンマ補正 ---
def apply_gamma_correction(img_rgb_norm, gamma=1.0):
    """RGB画像 (0-1正規化済み) にガンマ補正を適用する"""
    if img_rgb_norm is None:
        return None
    # ガンマ補正 (VチャンネルではなくRGB全体に適用する方が一般的で簡単)
    img_corrected = np.power(img_rgb_norm, 1.0 / gamma)
    # 値を0-1の範囲にクリップ
    img_corrected = np.clip(img_corrected, 0, 1)
    return img_corrected

# --- 5. 黒を青くする処理 ---
def make_blacks_blue(img_rgb_norm, threshold=0.2, blue_amount=0.1):
    """RGB画像 (0-1正規化済み) の黒い部分を青くする"""
    if img_rgb_norm is None:
        return None

    img_adjusted = img_rgb_norm.copy()
    # 黒に近いピクセルのマスクを作成
    # RGBすべての値が閾値以下の場合を黒と判定
    is_black_mask = np.all(img_adjusted <= threshold, axis=-1)

    # マスクされたピクセルの青チャンネル値を増やす
    img_adjusted[is_black_mask, 2] = np.clip(img_adjusted[is_black_mask, 2] + blue_amount, 0, 1)

    # オプション:青以外のチャンネルを少し減らすとより青みが強調される場合がある
    # img_adjusted[is_black_mask, 0] = np.clip(img_adjusted[is_black_mask, 0] - blue_amount * 0.1, 0, 1)
    # img_adjusted[is_black_mask, 1] = np.clip(img_adjusted[is_black_mask, 1] - blue_amount * 0.1, 0, 1)

    return img_adjusted

# --- 画像の読み込み (再実行) ---
try:
    img_pil_original = Image.open(image_path).convert('RGB')
    img_rgb_original = np.array(img_pil_original) / 255.0  # 0-1 の範囲に正規化
    print(f"画像 '{image_path}' を再度読み込みました。")
except FileNotFoundError:
    print(f"エラー: 画像ファイル '{image_path}' が見つかりません。")
    img_rgb_original = None
except Exception as e:
    print(f"画像の読み込み中にエラーが発生しました: {e}")
    img_rgb_original = None


# --- 処理の実行 ---
gamma_value = 1.2
black_threshold = 0.2 # 0-1 scale
blue_addition = 0.1   # 0-1 scale

img_gamma_corrected = None
img_final = None

if img_rgb_original is not None:
    # ガンマ補正を適用
    img_gamma_corrected = apply_gamma_correction(img_rgb_original, gamma=gamma_value)
    print(f"ガンマ補正 (gamma={gamma_value}) を適用しました。")

    # 黒を青くする処理を適用
    img_final = make_blacks_blue(img_gamma_corrected, threshold=black_threshold, blue_amount=blue_addition)
    print(f"黒を青くする処理 (threshold={black_threshold}, blue_amount={blue_addition}) を適用しました。")

# --- 6. 調整後のHSVヒストグラム表示 ---
if img_final is not None:
    plot_hsv_histograms(img_final, f"調整後のHSVヒストグラム (Gamma={gamma_value}, Blue={blue_addition})")

# --- 7. 結果画像の表示 ---
if img_rgb_original is not None and img_final is not None:
    fig, axs = plt.subplots(1, 2, figsize=(12, 6))
    axs[0].imshow(img_rgb_original)
    axs[0].set_title("元の画像")
    axs[0].axis('off')

    axs[1].imshow(img_final)
    axs[1].set_title(f"調整後の画像 (Gamma={gamma_value}, Blue={blue_addition})")
    axs[1].axis('off')

    plt.tight_layout()
    plt.show()

    # 調整後の画像をファイルに保存(オプション)
    # final_image_pil = Image.fromarray((img_final * 255).astype(np.uint8))
    # final_image_pil.save("output_image.png")
    # print("調整後の画像を 'output_image.png' として保存しました。")
Code execution result
画像 'input_file_0.jpeg' を再度読み込みました。
ガンマ補正 (gamma=1.2) を適用しました。
黒を青くする処理 (threshold=0.2, blue_amount=0.1) を適用しました。

after
after hist

Gemini 2.5 Pro:
ガンマ補正(全体的に少し明るくする)と、黒い部分に青みを加える処理を実行しました。調整後の画像と、調整後のHSVヒストグラムを表示しています。

**結果の考察:**

*   **調整後の画像:** 元の画像と比較して、全体的にわずかに明るくなり、特に髪の毛や服の濃い影の部分に青みが加わっているのが確認できます。
*   **調整後のHSVヒストグラム:**
    *   **Hue:** 黒い部分に青を加えたため、わずかに青系の色相(240度付近)が増えている可能性がありますが、ヒストグラム上では大きな変化は見えにくいです。
    *   **Saturation:** 黒い部分に色(青)を加えたため、彩度が0だったピクセルの一部がわずかに彩度を持つようになり、0付近のピークが少し減り、低い彩度の領域が若干増えている可能性があります。
    *   **Value:** ガンマ補正により全体的に明度が上がっているはずですが、元々明るいピクセルが非常に多いため、ヒストグラムの形状に劇的な変化は見られません。黒に近い部分(Valueが0に近い)のピクセルは、青みが加わることで若干明度が上がっている可能性もあります。

これらの調整(ガンマ値、黒と判定する閾値、加える青みの量)は、お好みに合わせて数値を変更することで、さらに効果を調整できます。例えば、より青みを強くしたい場合は `blue_addition` の値を大きくしたり、より暗い部分のみを対象にしたい場合は `black_threshold` の値を小さくしたりします。

ご希望に応じて、パラメータを変更して再実行することも可能です。

連続してコードを実行した場合、前のコードの情報を引き継げないため途中エラーが発生しましたが、エラー内容から軌道修正してちゃんとカラーバランス調整を行うことができました!!

おわりに

Google AI Studio + Code Execution + Gemini 2.5 Proを組み合わせることで簡単にPythonのコードを生成し実行できるだけではなく、PhotoshopやGIMP、Pixlrのような画像編集ソフトウェアを操作しなくてもAIが画像を解析して自動的に編集してくれるようになりました。

例えば以下のようなメリットがあります

  • Pythonで画像データを操作すると、画像生成AIより安定して加工できる
  • Gemini AppのようなWebアプリにあいまいなプロンプトを入力するだけなので、いつでもどこでもすぐ使える
  • 出力されたPythonのコードを保存することで処理を再現できる
  • ローカルにPythonの実行環境があれば変数や引数を変更するだけで調整でき、いくつかのデメリットを改善できる

ただ現時点では以下デメリットがあります。

  • AIなのでコードの出力が安定しない
  • Google AI Studioの画像や設定が保存されないため、再試行がやや面倒
  • Google Driveのアクセスを許可しないとチャット履歴が保存できない

特に画像編集において以下デメリットがあります。

  • PillowでSaveした画像ファイルを必ず表示してくれるとは限らないため、Matplotlibと組み合わせて使わなければならない
  • 生成したオリジナル画像を保存できず、表示された画像がJPEGにエンコードされているためどんどん劣化していく

なのでAIによって画像編集ソフトウェアが必要なくなったのではなく、10年前にOpenCVやscikit-learnで遊べなかった人がPillowとMatplotlibで気軽に遊べるようになっただけの話なのかもしれませんが、画像生成AIとは違った面白さがあるので興味があればぜひ遊んでみてください。

Discussion