0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LLMの「Lightroomで変更した写真のパラメータの推測能力」をDSPyを使って調べてみた

Last updated at Posted at 2026-01-03

背景にある興味・関心

自分はプログラミングに加えてカメラ・写真が趣味で,Adobe Lightroomで写真の編集(現像)をしています.Lightroomでは以下のようなパネルがあり,写真のパラメータを変更することができます.

image.png

この「現像」という行為,「やりすぎ」には批判がある一方で,現像によって写真の印象が大きく変わるという事実があります.
そこで一部の写真趣味のユーザーは「プリセット」という,自分好みの写真が出力できるパラメーターセットを作っていたりします.

自分も例に漏れずそのようなプリセットの調整をしながら写真の現像を楽しんでいますが,なかなか求める出来上がりが得られず苦戦することもしばしば.
そこで,この「写真の現像」にLLMを活用できないだろうか?と考えました.

例えば

  • 現像前の画像と実現したい写真のイメージを伝え,適切なパラメータを提案してもらう
  • 現像後の写真と変更したパラメータを伝え,追加の修正点を提案してもらう
  • 実現したい写真のイメージ・テイストを伝え,プリセットを作成してもらう

というようなことはできないだろうか?ということに興味を持ちました.

実験設定

上述のような興味・関心はありますが,いきなり「LLMを活用して現像のサジェストをしてくれるアプリを作りました」というのはハードルがあまりに高いと考えました.
そこで,まずはパラメータ変更前後の画像をLLMに渡し,どのパラメータが変更されたかをLLMに推測してもらうことを試してみることにしました.
**そもそもLLMが写真の「パラメータ」というものをどの程度理解できるのだろうか?**ということを確認することが目的です.

まず,ベースとなる編集前画像を用意しました.

00_base.jpg

これは,イタリア・カターニアで自分が撮影した写真です.
選定にあたって特に細かな条件があるわけではないのですが,

  • 明暗差が大きく,明るい部分・暗い部分がともに存在する
  • 様々な色の傘が存在するため,色の認識能力を測れそう

という理由で選定しました.

今回はあくまで1パラメータの変更の認識能力を測ることを目的として,例えば以下のような編集した画像を準備しました.

00_exposure_plus1.jpg

00_exposure_minus1.jpg

これは,先程の画像の「露出(exposure)」を+1したものと,-1したものです.
このような,一つのパラメータを+方向およびマイナス方向にそれぞれ変更した画像を,以下のパラメータについてそれぞれ作成しました

  • 色温度(ホワイトバランス)
  • 露出
  • コントラスト
  • ハイライト
  • シャドウ
  • 白レベル
  • 黒レベル
  • テクスチャ
  • 明瞭度
  • かすみの除去
  • 自然な彩度
  • 彩度
  • 色相(赤)
  • 彩度(赤)
  • 輝度(赤)

パラメータの変動量は,独断と偏見で「普段現像でこれ以上変更したら不自然になりそうだな,と思い止めるくらい」としました.完全に個人の独断と偏見です.

トーンカーブやカラーグレーディングについては,後述のDSPy上でのSignatureとしてアウトプットを定義することが難しいので,今回は検証対象から除外しました.
また,HSL(色相・彩度・輝度)については赤色のみ検証対象とし,それ以外の色については検証対象外としました.
これは単純に検証対象画像があまりに多いと費用影響が怖かったのと,現像をチマチマと手で行っていたので全色やるのが面倒になったためです.

評価方法

評価にあたっては,DSPyのEvaluateという機能を利用することにしました.
DSPyのEvaluateは主にOptimizeと組み合わせ,Optimize前後のスコアを比較し改善を確認することに使われることが多い印象ですが,今回はEvaluateのみ行い,Optimizeは行っていません.

以下のようなコードで評価を行っています

import dspy
from typing import Literal, Optional


class PhotoDevelopmentParameterAnalysis(dspy.Signature):
    """
    Given a base image and a developped image, identify the Lightroom parameters to adjust on the base image to match the developped image.
    """

    base_image: dspy.Image = dspy.InputField(desc="the base image")
    developped_image: dspy.Image = dspy.InputField(
        desc="the image which is developped by using Lightroom."
    )
    parameter: Literal[
        "whitebalance",
        "exposure",
        "contrast",
        "highlight",
        "shadow",
        "white_level",
        "black_level",
        "texture",
        "clarity",
        "dehaze",
        "vibrance",
        "saturation(overall)",
        "color_mixier_hue",
        "color_mixier_saturation",
        "color_mixier_brightness",
    ] = dspy.OutputField(
        desc="the Lightroom parameter which is adjusted on the base image to match the developped image."
    )
    color: Optional[Literal[
        "red", "orange", "yellow", "green", "aqua", "blue", "purple", "magenta"
    ]] = dspy.OutputField(
        desc="(optional) the color which is adjusted by color mixier - this field **must be filled only when the parameter is color_mixier_hue or color_mixer_saturation or color_mixer_brightness**"
    )
    direction: Literal["positive", "negative"] = dspy.OutputField(
        desc="the adjustment direction of selected parameter"
    )
import os
from dotenv import load_dotenv
import dspy
from langfuse import get_client
from openinference.instrumentation.dspy import DSPyInstrumentor
import time

import evaluation_data
from signatures import PhotoDevelopmentParameterAnalysis

# 環境変数を読み込む
load_dotenv()
OPENAI_API_KEY=os.getenv("OPENAI_API_KEY")
GEMINI_API_KEY=os.getenv("GEMINI_API_KEY")

# LangFuseでのトレースを有効にする
langfuse = get_client()
DSPyInstrumentor().instrument()

def validate_answer(eval_data, pred, trace=None):
    time.sleep(30)
    print("=============================================")
    print("correct parameter and direction is:" , eval_data.parameter, ",", eval_data.direction)
    print("answered parameter and direction is:" , pred.parameter, ",", pred.direction)
    if(eval_data.color or pred.color):
        print("correct color is:" , eval_data.color,)
        print("answered color is:" , pred.color)

    return int(eval_data.parameter == pred.parameter and eval_data.color == pred.color and eval_data.direction == pred.direction)


def main():
    # テスト用データの読み込み
    dataset = [dspy.Example({
        "base_image": dspy.Image(row["base_image"]),
        "developped_image": dspy.Image(row["developped_image"]),
        "parameter": row["parameter"],
        "color": row["color"],
        "direction": row["direction"]
    }).with_inputs("base_image", "developped_image") for row in evaluation_data.evaluation_data]


    with dspy.context(
        lm=dspy.LM(
            #"bedrock/global.anthropic.claude-sonnet-4-5-20250929-v1:0", cache=False, rpm=0.5,
            #"bedrock/global.anthropic.claude-opus-4-5-20251101-v1:0", cache=False, rpm=0.5,
            # "bedrock/us.amazon.nova-2-lite-v1:0", cache=False, rpm=5,
            #"openai/gpt-5.2-2025-12-11", api_key=OPENAI_API_KEY, cache=False, rpm=0.5,
            "gemini/gemini-3-pro-preview", api_key=GEMINI_API_KEY, cache=False, rpm=0.5,
        )
    ):
        analyser = dspy.ChainOfThought(PhotoDevelopmentParameterAnalysis)
        evaluate = dspy.Evaluate(devset=dataset, metric=validate_answer, num_thread=1, display_table=True, display_progress=True)
        evaluate(analyser)



if __name__ == "__main__":
    main()


推論のモジュールとしてはChainOfThoughtを使用し,PhotoDevelopmentParameterAnalysisで定義したSignatureをIOの定義としています.

また,モデルにClaude Sonnet4.5/Opus4.5 on Bedrockを使う際に,レートリミットに抵触するエラーが出たため,evaluateの関数内で強制的に30秒スリープさせることで回避しています.
(rpmを下げてもレートリミット抵触が止まらなかったため.少し調べたが原因がよく分からず,確実に止められる手法を採用しました.)

結果

今回は以下のモデルを対象に評価を実行しました.

  • Claude Sonnet 4.5
  • Claude Opus 4.5
  • Nova 2 Lite
  • GPT 5.2
  • Gemini 3 Pro

結果としては以下の通りとなりました.

image.png

表の見方ですが,

  • 左側に正解
  • 3列目以降が各モデルの結果
  • 緑セルが正解・赤セルが不正解

つまり例えば

image.png

これは,

  • ベースイメージに対して色温度を+方向に振った画像を,Opus4.5は「色温度が+になっている」と評価した(正解)
  • ベースイメージに対して色温度を-方向に振った画像を,Opus4.5は「ブルー彩度が+になっている」と評価した(不正解)

を意味します.

考察

まず,総合評価としてはGemini 3 Proの優秀さが光りました.
他がせいぜい10%,よくて20%弱の正解率の中,唯一40%の正解率を叩き出しています.

個別の結果を見ると,

  • Claudeはシャドウの強弱で画像の変化を説明しようとする傾向が強い
  • Nova 2 LiteおよびGPT 5.2はClaudeと比べると多彩なパラメータで変化を説明しようとする傾向が強い

ように思われます.
これは一概に「間違っている」と言うわけではなく,実際に「シャドウを持ち上げた結果色が出やすくなり,彩度が上がったように見える」ということは十分にあり得ます.
また,ハイライトと白レベル/シャドウと黒レベルは非常に違いが分かりづらく,人間が見ても区別できるわけではないと思います.
従って,ここで不正解になっている=画像の変化を全く認識できていないと言い切れるわけではありません.

一方で,

  • 正解がシャドウ・+なのにシャドウ・-と評価した

のような,全く正反対の不正解な結果が得られた箇所もあります(e.g. Sonnet 4.5のシャドウ).

また,

  • パラメータAが+の場合をパラメータB+
  • パラメータAが-の場合をパラメータB+

と評価している(e.g. Opusのレッド輝度)例も見られました.
(パラメータAの変更によりパラメータBが変更されたように見えたとしても,+-で同じ方向に変化するように見えるのはいささか不可解)

このようなケースがあるモデルは,いくら他のパラメータの成功率が高かったとしても,全体として優秀とは言い難いように感じます.
(喩えとして適切かはわかりませんが,喩えるなら禁忌肢のようなもの)

その点,Gemini 3 Proはこういった大外しが無い(上に正答も多い)という点が評価に値すると言えます.

今回の調査の限界と今後に向けて

今回の調査にはお察しの通り大量の限界があります.例えば,

  • 試行回数不足
    • 各モデル×パラメータ×方向性の組み合わせを1回しか試行していない
  • パラメータの変更量の大きさのバリエーション不足
    • 独断と偏見による「これくらい変えると不自然になるだろう」で決めた1パターンのみ
  • 題材とした画像のバリエーション不足
    • カターニアの街角の1枚のみ

これらは,統計的に満足できる程度の試行回数を繰り返すとLLMへの課金でまあまあな金額が吹っ飛ぶと予想されたため,というお財布的な制約条件によるものです(世知辛い).

今後は,今回の限られたサンプルではあるもののGemini 3 Proが優秀そうであることがわかったため,Gemini 3 Proにスポットを当てて調査・検討を進めていきたいと考えています.
具体的には

  • Gemini 3 Proに絞って他の写真のサンプルで評価したり,試行回数を増やしてみる
  • Chain-of-Thoughtのreasoningの中身を分析してみる

と言ったことが実施できると考えています.

一方で今回の結果は,(今後のプロンプトの改善等で改善する余地はあるものの)1パラメータの変更に絞ったとしても,現行のモデルではその変化を正確に把握するのは難しそうである,という限界も見えた結果だと思います.

そこで,今後はただ「画像を比較させて検討させる」というアプローチ以外のアプローチも検討してみる必要があるかもしれません(が,具体的なアイディアはまだ出ていません).

ソースコード

今回実行した評価のソースコードは以下にあります.
評価用の画像も含めています.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?