XMPメタデータを使用してPythonでRAW画像をJPEGに変換してみた

※ 残念ながら、lightroom現像の再現には失敗しています。
※ もう少し実際の現像状況を見つつ、設定項目を再検証する必要あり（してからここに上げろよって話ではある）

## 前提条件
　RAW画像ファイルはカメラが撮影した未加工のデータを含むため、高品質な画像編集が可能ですが、そのままでは扱いにくく、実際に画像として使用する際には、JPEG形式に変換する必要があります。

　私は通常、lightroomにて現像を行っていますが、このプリセットファイルは出力することができ、各設定情報は簡単に読み解くことが可能です。また、簡単にJSONなどのファイルに変換することができます。

　今回は、lightroomのプリセットファイルであるXMPファイルを参照し、Pythonを用いてRAWファイルを現像してみます。RAWファイルはオリンパスのDNG形式、lightroomのプリセットファイルは私のオリジナルのものを使用します。

　以下は今回使用したファイルのリンクです。
　・RAWファイル：https://drive.google.com/file/d/15miXXxP8mv_yEo8fiOLNYS-56k59ua5t/view?usp=drive_link
　・プリセットファイル：https://drive.google.com/file/d/16B5S0_Lyb5mf3vZRJg4pMBZ2nClcfcqR/view?usp=drive_link

## ライブラリ
今回のプログラムに使用するライブラリの一覧です。

・rawpy
LibRawライブラリのPythonラッパーであり、RAW画像の処理を簡単に行うことができます。
また、ホット/デッドピクセルの修復機能も備えています。
・imageio
imageioは、幅広い画像およびビデオデータを読み書きするための簡単なインターフェースを提供するPythonライブラリです。アニメーション画像や科学データなどもサポートしています。
・lxml
lxmlは、PythonでXMLおよびHTMLを処理するための最も機能豊富で使いやすいライブラリです。libxml2およびlibxsltのCライブラリに基づいており、高速で完全なXML機能を提供します。

## プログラムの各パーツについて
### 1. XMPファイルをJSONに変換
XMPファイルを読み込み、XML形式のデータをパースしてJSON形式に変換します。
```python:mushi.py
def xmp_to_json(xmp_path):
    with open(xmp_path, 'rb') as file:
        xmp_data = file.read()
    root = etree.fromstring(xmp_data)
    xmp_dict = {child.tag: child.text.strip() for child in root.iter() if child.text}
    return xmp_dict
```

### 2. JSONを保存
変換されたJSONデータを指定されたディレクトリに保存します。
```python:mushi.py
def save_json(data, output_dir, xmp_filename):
    base_filename = os.path.splitext(os.path.basename(xmp_filename))[0]
    json_path = os.path.join(output_dir, f"{base_filename}.json")
    with open(json_path, 'w', encoding='utf-8') as json_file:
        json.dump(data, json_file, ensure_ascii=False, indent=4)
```

### 3. RAWファイルをJPEGに現像
RAWファイルを読み込み、XMPファイルの設定を適用してJPEG形式に変換します。
```python:mushi.py
def convert_raw_to_jpeg(dng_path, output_dir, base_filename, xmp_data):
    with rawpy.imread(dng_path) as raw:
        params = rawpy.Params()

        # XMPデータを適用するための処理
        if '{http://ns.adobe.com/camera-raw-settings/1.0/}Exposure2012' in xmp_data:
            params.exposure = float(xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Exposure2012']) if xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Exposure2012'] else 0.0
        # 他のパラメータも同様に設定

        rgb = raw.postprocess(params=params)
        output_path = os.path.join(output_dir, f"{base_filename}.jpg")
        imageio.imwrite(output_path, rgb, quality=95)  # 高画質で保存
```
このコードでは、XMPファイルから取得したメタデータを使用して、RAW画像の現像パラメータを設定しています。
以下に、具体的な処理について詳しく説明します。
### Exposure2012の適用
```python:mushi.py
if '{http://ns.adobe.com/camera-raw-settings/1.0/}Exposure2012' in xmp_data:
    params.exposure = float(xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Exposure2012']) if xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Exposure2012'] else 0.0
```
　この部分のコードは、XMPファイル内のExposure2012というタグを探し、その値をparams.exposureに設定しています。Exposure2012は、Adobe Camera RawやLightroomで使用される露出補正の値を示します。この値を適用することで、RAW画像の露出を調整します。
　具体的には、以下の手順で処理が行われます
　　1. XMPデータ内に"{http</foo>://</foo>ns.adobe.com/camera-raw-settings/1.0/}Exposure2012"というキーが存在するか確認します。
　　2. 存在する場合、その値を取得し、浮動小数点数に変換してparams.exposureに設定します。
　　3. 値が存在しない場合は、デフォルト値として0.0を設定します。

## まとめ
いちおう、コード自体の指示通りには現像できました。
が、やはり色味がかなり違っています…うーむ…
シャドウとホワイトバランスは明らかにトチッてますね。
他にもいろいろ再検証する必要があるようです。
 
未処理のjpeg
![IMG_20240517_185403 (2).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677892/ac0035df-7567-183c-bbb1-7199773e70d9.jpeg)


lightroomで現像したjpegファイル
![IMG_20240517_185403 (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677892/d829ecfa-066b-a75a-b43d-c1351c840fb5.jpeg)


本コードで現像したjpegファイル
![IMG_20240517_185403.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677892/9c6e7c33-fd21-b3e9-1324-525ea7d22283.jpeg)


## 参考サイト
今回参考にしたサイト集です。

[Adobe XMP Specification](https://developer.adobe.com/xmp/docs/XMPSpecifications/https://developer.adobe.com/xmp/docs/XMPSpecifications/)	
　:XMPの仕様について詳細に説明しています。
[Adobe Experience Manager](https://experienceleague.adobe.com/ja/docs/experience-manager-cloud-service/content/assets/admin/xmp-metadata)	
　:XMPメタデータの概要とエコシステムについて解説しています。
[Accusoft Documentation](https://help.accusoft.com/ImageGear/v26.3/xmp-metadata-structure.html)	
　:XMPメタデータの構造について説明しています。
[Post Partner Blog](https://www.postpartner.com/blog/how-to-apply-xmps-to-raw-images)	
　:XMPファイルをRAW画像に適用する方法についてのステップバイステップガイド。
[Damien Symonds Blog](https://www.damiensymonds.net/2012/02/what-is-xmp-data-all-about.html)	
　:XMPデータの基本とその役割について解説しています。

## コード
```python:mushi.py
import rawpy
import imageio
import json
import os
from lxml import etree

# ファイルパスの設定
dng_file = r"C:\Users\\Documents\.dng"
xmp_file = r"C:\Users\\Documents\.xmp"
output_dir = r"C:\Users\\Documents"

# 1. XMPファイルをJSONに変換
def xmp_to_json(xmp_path):
    with open(xmp_path, 'rb') as file:
        xmp_data = file.read()
    root = etree.fromstring(xmp_data)
    xmp_dict = {child.tag: child.text.strip() for child in root.iter() if child.text}
    return xmp_dict

# 2. JSONを保存
def save_json(data, output_dir, xmp_filename):
    base_filename = os.path.splitext(os.path.basename(xmp_filename))[0]
    json_path = os.path.join(output_dir, f"{base_filename}.json")
    with open(json_path, 'w', encoding='utf-8') as json_file:
        json.dump(data, json_file, ensure_ascii=False, indent=4)

# 3. RAWファイルをJPEGに現像
def convert_raw_to_jpeg(dng_path, output_dir, base_filename, xmp_data):
    with rawpy.imread(dng_path) as raw:
        params = rawpy.Params()

        # XMPデータを適用するための処理
        if '{http://ns.adobe.com/camera-raw-settings/1.0/}Exposure2012' in xmp_data:
            params.exposure = float(xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Exposure2012']) if xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Exposure2012'] else 0.0
        if '{http://ns.adobe.com/camera-raw-settings/1.0/}Contrast2012' in xmp_data:
            params.contrast = float(xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Contrast2012']) if xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Contrast2012'] else 0.0
        if '{http://ns.adobe.com/camera-raw-settings/1.0/}Highlights2012' in xmp_data:
            params.highlight = float(xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Highlights2012']) if xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Highlights2012'] else 0.0
        if '{http://ns.adobe.com/camera-raw-settings/1.0/}Shadows2012' in xmp_data:
            params.shadow = float(xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Shadows2012']) if xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Shadows2012'] else 0.0
        if '{http://ns.adobe.com/camera-raw-settings/1.0/}Whites2012' in xmp_data:
            params.white = float(xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Whites2012']) if xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Whites2012'] else 0.0
        if '{http://ns.adobe.com/camera-raw-settings/1.0/}Blacks2012' in xmp_data:
            params.black = float(xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Blacks2012']) if xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Blacks2012'] else 0.0
        if '{http://ns.adobe.com/camera-raw-settings/1.0/}Clarity2012' in xmp_data:
            params.clarity = float(xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Clarity2012']) if xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Clarity2012'] else 0.0
        if '{http://ns.adobe.com/camera-raw-settings/1.0/}Vibrance' in xmp_data:
            params.vibrance = float(xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Vibrance']) if xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Vibrance'] else 0.0
        if '{http://ns.adobe.com/camera-raw-settings/1.0/}Saturation' in xmp_data:
            params.saturation = float(xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Saturation']) if xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Saturation'] else 0.0
        if '{http://ns.adobe.com/camera-raw-settings/1.0/}Temperature' in xmp_data:
            params.temperature = float(xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Temperature']) if xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Temperature'] else 0.0
        if '{http://ns.adobe.com/camera-raw-settings/1.0/}Tint' in xmp_data:
            params.tint = float(xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Tint']) if xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}Tint'] else 0.0
        if '{http://ns.adobe.com/camera-raw-settings/1.0/}ToneCurvePV2012' in xmp_data:
            params.tone_curve = [float(i) for i in xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}ToneCurvePV2012'].split(',') if i]
        if '{http://ns.adobe.com/camera-raw-settings/1.0/}ToneCurvePV2012Red' in xmp_data:
            params.tone_curve_red = [float(i) for i in xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}ToneCurvePV2012Red'].split(',') if i]
        if '{http://ns.adobe.com/camera-raw-settings/1.0/}ToneCurvePV2012Green' in xmp_data:
            params.tone_curve_green = [float(i) for i in xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}ToneCurvePV2012Green'].split(',') if i]
        if '{http://ns.adobe.com/camera-raw-settings/1.0/}ToneCurvePV2012Blue' in xmp_data:
            params.tone_curve_blue = [float(i) for i in xmp_data['{http://ns.adobe.com/camera-raw-settings/1.0/}ToneCurvePV2012Blue'].split(',') if i]

        rgb = raw.postprocess(params=params)
        output_path = os.path.join(output_dir, f"{base_filename}.jpg")
        imageio.imwrite(output_path, rgb, quality=95)  # 高画質で保存

# メイン処理
def main():
    base_filename = os.path.splitext(os.path.basename(dng_file))[0]
    
    # XMPをJSONに変換
    xmp_data = xmp_to_json(xmp_file)
    
    # JSONを保存
    save_json(xmp_data, output_dir, xmp_file)
    
    # RAWファイルをJPEGに変換
    convert_raw_to_jpeg(dng_file, output_dir, base_filename, xmp_data)

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