PythonでRAW現像！

PythonでRAW画像を読み込んで簡単な編集をしてJPEGに書き出す方法を解説します

## RAW画像の読み込み

```python
import rawpy
import numpy as np
from PIL import Image
from scipy import interpolate
from matplotlib import pyplot as plt
import matplotlib.colors as mcolors

def read_raw_image(file_path: str) -> np.ndarray:
    # rawpyで読み込み
    rawpy_object = rawpy.imread(file_path) 

    # rawpyでRAW画像を16bit RGB画像に変換
    image_array: np.ndarray = rawpy_object.postprocess(use_camera_wb = True, output_bps=16).astype(np.float32)

    # 0~1の範囲にする

    image_array = (image_array / 65535.0).astype(np.float32)

    return image_array


path = r"ファイルパス"
image_array = read_raw_image(path)
```

## 画像の表示

```python
def display(image_array: np.ndarray):
    # pillowのImgaeオブジェクトにして表示
    Image.fromarray(np.clip(image_array * 255, 0, 255).astype(np.uint8)).show()

display(image_array)
```

## 色や明るさを調整するためHLS ⇔ RGB変換
opencvで変換を試してみましたがエラーは出ないもののうまく変換できませんでした。
uint8でしか動かないみたいです。

なのでAIに書いてもらいました

```python
def rgb_to_hls_image(rgb_image: np.ndarray) -> np.ndarray:
    """
    RGB画像をHLS画像に変換する関数
    """
    # 入力が3次元配列（高さ、幅、チャンネル）であることを確認
    if rgb_image.ndim != 3 or rgb_image.shape[2] != 3:
        raise ValueError("入力は3チャンネルのRGB画像である必要があります")
    
    rgb_normalized = rgb_image.copy()
    
    r, g, b = rgb_normalized[:,:,0], rgb_normalized[:,:,1], rgb_normalized[:,:,2]
    max_c = np.maximum(np.maximum(r, g), b)
    min_c = np.minimum(np.minimum(r, g), b)
    
    l = (max_c + min_c) / 2
    s = np.zeros_like(l)
    h = np.zeros_like(l)
    
    delta = max_c - min_c
    
    
    mask = (max_c != min_c)
    s[mask] = np.where(l[mask] > 0.5, 
                    delta[mask] / (2 - max_c[mask] - min_c[mask]),
                    delta[mask] / (max_c[mask] + min_c[mask]))
    
    
    mask_r = (max_c == r) & mask
    h[mask_r] = (g[mask_r] - b[mask_r]) / delta[mask_r] + np.where(g[mask_r] < b[mask_r], 6, 0)
    
    mask_g = (max_c == g) & mask
    h[mask_g] = (b[mask_g] - r[mask_g]) / delta[mask_g] + 2
    
    mask_b = (max_c == b) & mask
    h[mask_b] = (r[mask_b] - g[mask_b]) / delta[mask_b] + 4
    
    h /= 6
    
    
    
    
    return np.dstack((h, l, s)).astype(np.float32)

def hls_to_rgb_image(hls_image: np.ndarray) -> np.ndarray:
    """
    HLS画像をRGB画像に変換する関数
    """
    if hls_image.ndim != 3 or hls_image.shape[2] != 3:
        raise ValueError("入力は3チャンネルのHLS画像である必要があります")

    
    h = hls_image[:, :, 0]
    l = hls_image[:, :, 1]
    s = hls_image[:, :, 2]

    
    q = np.where(l < 0.5, l * (1 + s), l + s - l * s)
    p = 2 * l - q

    
    def _hue2rgb(p, q, t):
        
        t = t % 1.0 
        res = np.empty_like(t)
        
        res[(t < 1/6)] = p[(t < 1/6)] + (q[(t < 1/6)] - p[(t < 1/6)]) * 6 * t[(t < 1/6)]
        res[(1/6 <= t) & (t < 1/2)] = q[(1/6 <= t) & (t < 1/2)]
        res[(1/2 <= t) & (t < 2/3)] = p[(1/2 <= t) & (t < 2/3)] + (q[(1/2 <= t) & (t < 2/3)] - p[(1/2 <= t) & (t < 2/3)]) * (4 - 6 * t[(1/2 <= t) & (t < 2/3)])
        res[(2/3 <= t)] = p[(2/3 <= t)]
        return res

    r = _hue2rgb(p, q, h + 1/3)
    g = _hue2rgb(p, q, h)
    b = _hue2rgb(p, q, h - 1/3)

    rgb = np.stack([r, g, b], axis=2)
    
    return np.clip(rgb, 0, 1).astype(np.float32)
```

## 明るさ調整 トーンカーブ！！

```python
def apply_tone_curve(hls_image_array: np.ndarray, control_points_x: list[int], control_points_y: list[int]):
    """明るさ調整トーンカーブ"""
    if control_points_x is None or control_points_y is None:
        raise ValueError("x, yの制御点を指定してください")
    
    if len(control_points_x) != len(control_points_y):
        raise ValueError("制御点の数が一致していません")
    # scipyの補間で曲線トーンカーブを生成
    f = interpolate.PchipInterpolator(control_points_x, control_points_y)
    curve = np.clip(f(np.arange(65536)), 0, 65535).astype(np.uint16)

    
    # トーンカーブのグラフを作る
    plt.plot(list(range(0, 65536)), curve)
        
    
    for x in control_points_x:
        plt.plot(x, curve[x], marker='.', color='red')
    plt.grid(True)
    plt.savefig("明るさトーンカーブ.png")
    plt.close()

    # トーンカーブ適用
    hls_image = (hls_image_array.copy() * 65535).astype(np.int32)

    hls_image[:,:,1] = curve[hls_image[:, :, 1]]

    return (hls_image / 65535.0).astype(np.float32)
    
    

# 読み込み
image_array = read_raw_image(path)

# 明るさ調整するためにHLSに変換
hls_image = rgb_to_hls_image(image_array) 

# トーンカーブを適用
hls_image = apply_tone_curve(hls_image, [0, 23000, 65535], [0, 36000, 65535])

# RGBに戻す
rgb_image = hls_to_rgb_image(hls_image)


# 表示
display(rgb_image)
```

## 色調整 色ごとに彩度を調整！
```python
def apply_hls_saturation_tone_curve(hls_image_array: np.ndarray, control_points_x: list[int], control_points_y: list[int]):
    """彩度調整トーンカーブ"""
    if len(control_points_x) != len(control_points_y):
        raise ValueError("制御点の数が一致していません")
    
    # scipyの補間で曲線トーンカーブを生成
    f = interpolate.PchipInterpolator(np.array(control_points_x) * 182, np.array(control_points_y) / 100 * 65535)
    
    # 0〜65535の範囲で彩度調整カーブを生成
    curve = np.clip(f(np.arange(65536)), 0, 131070).astype(np.float32)

    # カーブをプロット
    plt.figure(figsize=(12, 8))
    
    # 背景に色相環を表示
    for i in range(360):
        color = mcolors.hsv_to_rgb([i/360, 1, 1])
        plt.axvline(i, color=color, alpha=0.2)

    # 彩度調整カーブをプロット
    x = np.arange(65536) / 182
    y = curve / 655.35
    plt.plot(x, y, 'k-', linewidth=2)
    
    if curve is None:
        # 制御点をプロット
        for x, y in zip(control_points_x, control_points_y):
            color = mcolors.hsv_to_rgb([x/360, 1, 1])
            plt.plot(x, y, 'o', color=color, markersize=10)

    plt.xlabel('色相')
    plt.ylabel('彩度 (%)')
    plt.title('彩度調整カーブ')
    plt.xlim(0, 360)
    plt.ylim(0, 200)
    plt.grid(True, alpha=0.3)
    
    # y軸に目盛りを追加
    y_ticks = [0, 50, 100, 150, 200]
    plt.yticks(y_ticks)
    
    # 100%のラインを追加
    plt.axhline(100, color='r', linestyle='--', alpha=0.5)
    plt.text(365, 100, '100%', va='center', ha='left', color='r')

    plt.savefig("彩度調整カーブ.png", dpi=300, bbox_inches='tight')
    plt.close()

    # 彩度チャンネル（S）に調整を適用
    hls_image = hls_image_array.copy() * 65535.0

    
    
    hue = hls_image[:,:,0].astype(np.uint16)
    hls_image[:,:,2] = np.clip(hls_image[:,:,2] * curve[hue] / 65535, 0, 65535)
    

    return (hls_image / 65535.0).astype(np.float32)
    

hls_image = apply_hls_saturation_tone_curve(hls_image, [0, 360], [160, 160])

# [0, 360]は色相、[160, 160]はその色相の場所の彩度の倍数 (100で変更なし、50で半分、200で2倍)

```


##### 明るさと彩度トーンカーブを作りましたが、彩度のほかに輝度や色相の調整トーンカーブも簡単に作れます。

## JPEGに書き出し

```python
Image.fromarray(np.clip(rgb_image * 255.0, 0.0, 255.0).astype(np.uint8)).save("編集した写真.jpeg", optimize=True, quality=90)
```

## 終わりに

Pythonはライブラリが強いのでraw画像読み込み用のrawpyやnumpyなどがあるので他の言語にくらべて比較的簡単にRAW現像が作れるのでやってみてください！